<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>LDAP Schema Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/ldap-schema/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/ldap-schema/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:38:33 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>LDAP Schema Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/ldap-schema/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>LDAP Internals: The Directory Tree, Schema, and What Travels on the Wire</title>
		<link>https://linuxcent.com/ldap-internals-directory-structure/</link>
					<comments>https://linuxcent.com/ldap-internals-directory-structure/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 24 Apr 2026 19:41:04 +0000</pubDate>
				<category><![CDATA[Identity & Authentication]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Directory Services]]></category>
		<category><![CDATA[Identity Management]]></category>
		<category><![CDATA[LDAP]]></category>
		<category><![CDATA[LDAP Schema]]></category>
		<category><![CDATA[ldapsearch]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://linuxcent.com/ldap-internals-directory-structure/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span>Understand LDAP internals: the DIT hierarchy, DN syntax, object classes, schema, and the BER-encoded bytes that travel from directory server to authentication daemon.</p>
<p>The post <a href="https://linuxcent.com/ldap-internals-directory-structure/">LDAP Internals: The Directory Tree, Schema, and What Travels on the Wire</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>The Identity Stack, Episode 2</em><br />
<a href="/what-is-ldap/">EP01: What Is LDAP</a> → <strong>EP02</strong> → <a href="/ldap-authentication-linux-pam-nss/">EP03: LDAP Authentication on Linux</a> → &#8230;</p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>The Directory Information Tree (DIT) is the hierarchical database LDAP stores — every entry lives at a unique path described by its Distinguished Name (DN)</li>
<li>Object classes define what attributes an entry is allowed or required to have — <code class="" data-line="">posixAccount</code> adds UID, GID, and home directory; <code class="" data-line="">inetOrgPerson</code> adds email and display name</li>
<li>Schema is the rulebook: which attribute types exist across the entire directory, what syntax each follows, and which object classes require or permit them</li>
<li>An LDAP Search sends four things: a base DN, a scope (base/one/sub), a filter like <code class="" data-line="">(uid=vamshi)</code>, and a list of attributes to return — the server traverses the tree and returns LDIF</li>
<li>Every LDAP message on the wire is BER-encoded (Basic Encoding Rules, a subset of ASN.1) — a compact binary format, not text</li>
<li><code class="" data-line="">ldapsearch</code> output is LDIF (LDAP Data Interchange Format) — the human-readable representation of what the BER payload carried</li>
</ul>
<hr />
<h2 id="the-big-picture-from-ldapsearch-to-directory-entry">The Big Picture: From ldapsearch to Directory Entry</h2>
<pre><code class="" data-line="">ldapsearch -x -H ldap://dc.corp.com -b &quot;dc=corp,dc=com&quot; &quot;(uid=vamshi)&quot; cn mail uidNumber
     │
     │  TCP port 389 (or 636 for LDAPS)
     │  BER-encoded SearchRequest
     ▼
┌─────────────────────────────────────────────────┐
│  LDAP Server (AD / OpenLDAP / 389-DS / FreeIPA)  │
│                                                   │
│  Directory Information Tree                       │
│                                                   │
│  dc=corp,dc=com                    ← search base  │
│    └── ou=engineers                ← scope: sub   │
│          ├── uid=alice                            │
│          └── uid=vamshi  ← filter match           │
│                cn: vamshi                         │
│                mail: vamshi@corp.com              │
│                uidNumber: 1001                    │
└─────────────────────────────────────────────────┘
     │
     │  BER-encoded SearchResultEntry
     ▼
# LDIF output on your terminal
dn: uid=vamshi,ou=engineers,dc=corp,dc=com
cn: vamshi
mail: vamshi@corp.com
uidNumber: 1001
</code></pre>
<p>LDAP internals are the mechanics between the command you type and the directory entry you get back. EP01 explained why LDAP was invented. This episode explains what it actually does when you run it.</p>
<hr />
<h2 id="the-directory-information-tree">The Directory Information Tree</h2>
<p>EP01 introduced the DIT as a concept inherited from X.500. Here&#8217;s what it actually looks like inside a directory.</p>
<p>Every LDAP directory has a root — the base DN — from which all entries descend. For a company called Corp with a domain <code class="" data-line="">corp.com</code>, the base is typically <code class="" data-line="">dc=corp,dc=com</code>. Below that, the tree branches into organizational units, and below those, individual entries for people, groups, services, and anything else the directory administrator decided to model.</p>
<pre><code class="" data-line="">dc=corp,dc=com                          ← domain root (base DN)
│
├── ou=people                           ← organizational unit: people
│     ├── uid=alice                     ← user entry
│     ├── uid=vamshi
│     └── uid=bob
│
├── ou=groups                           ← organizational unit: groups
│     ├── cn=engineers
│     └── cn=ops
│
├── ou=services                         ← organizational unit: service accounts
│     ├── cn=jenkins
│     └── cn=gitlab-runner
│
└── ou=hosts                            ← organizational unit: machines
      ├── cn=web01.corp.com
      └── cn=db01.corp.com
</code></pre>
<p>This hierarchy is not a file system and not a relational database. It is specifically optimized for reads — the query &#8220;give me everything about this user&#8221; is the operation the protocol is built around. Writes are infrequent. Reads are constant.</p>
<p>Every entry in the tree has exactly one parent. There are no cross-links between branches, no foreign keys. The tree is the structure. An entry&#8217;s position in the tree is what defines it.</p>
<hr />
<h2 id="distinguished-names-reading-the-path">Distinguished Names: Reading the Path</h2>
<p>The Distinguished Name (DN) is how you address any entry in the directory. It reads right-to-left, from the leaf to the root, with each component separated by a comma.</p>
<pre><code class="" data-line="">uid=vamshi,ou=engineers,dc=corp,dc=com

Reading right-to-left:
  dc=corp,dc=com       ← domain: corp.com
  ou=engineers         ← organizational unit: engineers
  uid=vamshi           ← this specific entry: user &quot;vamshi&quot;
</code></pre>
<p>Each component of a DN — <code class="" data-line="">uid=vamshi</code>, <code class="" data-line="">ou=engineers</code>, <code class="" data-line="">dc=corp</code> — is a Relative Distinguished Name (RDN). The RDN is the attribute-value pair that uniquely identifies the entry within its parent container. Two users in the same <code class="" data-line="">ou=engineers</code> cannot both have <code class="" data-line="">uid=vamshi</code> — that would create two entries with identical DNs, which the directory won&#8217;t allow.</p>
<p>Common RDN attribute types and what they mean:</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Stands for</th>
<th>Typical use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">dc</code></td>
<td>Domain Component</td>
<td>Domain name segments (<code class="" data-line="">dc=corp,dc=com</code> = corp.com)</td>
</tr>
<tr>
<td><code class="" data-line="">ou</code></td>
<td>Organizational Unit</td>
<td>Container for grouping entries</td>
</tr>
<tr>
<td><code class="" data-line="">cn</code></td>
<td>Common Name</td>
<td>Groups, service accounts, human-readable name</td>
</tr>
<tr>
<td><code class="" data-line="">uid</code></td>
<td>User ID</td>
<td>Linux username — the standard RDN for user entries</td>
</tr>
<tr>
<td><code class="" data-line="">o</code></td>
<td>Organization</td>
<td>Top-level org containers (less common in modern setups)</td>
</tr>
</tbody>
</table>
<p>When your Linux system calls <code class="" data-line="">getent passwd vamshi</code>, SSSD translates that into an LDAP Search for an entry where <code class="" data-line="">uid=vamshi</code> somewhere under the configured base DN. The full DN comes back with the result, but what your system cares about are the attributes inside it.</p>
<hr />
<h2 id="object-classes-and-schema">Object Classes and Schema</h2>
<p>Every entry in the directory has a <code class="" data-line="">objectClass</code> attribute — usually several values. Object classes define what attributes the entry is allowed or required to have.</p>
<pre><code class="" data-line=""># A typical user entry&#039;s object classes
dn: uid=vamshi,ou=engineers,dc=corp,dc=com
objectClass: top
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
</code></pre>
<p>Each object class contributes a set of attributes — some required (<code class="" data-line="">MUST</code>), some optional (<code class="" data-line="">MAY</code>):</p>
<pre><code class="" data-line="">objectClass: posixAccount
  MUST: cn, uid, uidNumber, gidNumber, homeDirectory
  MAY:  userPassword, loginShell, gecos, description

objectClass: inetOrgPerson
  MUST: sn (surname), cn
  MAY:  mail, telephoneNumber, displayName, jpegPhoto, ...

objectClass: shadowAccount
  MUST: uid
  MAY:  shadowLastChange, shadowMin, shadowMax, shadowWarning, ...
</code></pre>
<p>When Linux authenticates a user via LDAP, it needs the <code class="" data-line="">posixAccount</code> attributes: <code class="" data-line="">uidNumber</code> (the numeric UID), <code class="" data-line="">gidNumber</code>, <code class="" data-line="">homeDirectory</code>, and <code class="" data-line="">loginShell</code>. Without <code class="" data-line="">posixAccount</code>, the user entry exists in the directory but can&#8217;t be used for Linux logins — <code class="" data-line="">getent passwd</code> will return nothing.</p>
<p>Object classes are grouped into three kinds:</p>
<p>Groups in LDAP use their own object class:</p>
<pre><code class="" data-line="">objectClass: groupOfNames
  MUST: cn, member
  MAY:  description, owner, ...

# A group entry looks like this:
dn: cn=engineers,ou=groups,dc=corp,dc=com
objectClass: groupOfNames
cn: engineers
member: uid=vamshi,ou=engineers,dc=corp,dc=com
member: uid=alice,ou=engineers,dc=corp,dc=com
</code></pre>
<p><code class="" data-line="">groupOfNames</code> stores members as full DNs — which is why the SSSD group search filter is <code class="" data-line="">(member=uid=vamshi,ou=...)</code> rather than <code class="" data-line="">(member=vamshi)</code>. The directory stores the exact path to each member entry. <code class="" data-line="">posixGroup</code> is the alternative, which stores the <code class="" data-line="">memberUid</code> as a bare username string instead of a DN — Active Directory uses <code class="" data-line="">groupOfNames</code>; pure POSIX environments often use <code class="" data-line="">posixGroup</code>.</p>
<p>Object classes are grouped into three kinds:</p>
<p><strong>Structural</strong> — defines what the entry fundamentally is. Every entry must have exactly one structural class. <code class="" data-line="">posixAccount</code> is structural.</p>
<p><strong>Auxiliary</strong> — adds additional attributes to an existing entry. <code class="" data-line="">shadowAccount</code> and <code class="" data-line="">inetOrgPerson</code> can be auxiliary. You can stack multiple auxiliary classes on a single entry.</p>
<p><strong>Abstract</strong> — base classes that other classes inherit from. <code class="" data-line="">top</code> is the root abstract class that every entry implicitly has. You never add <code class="" data-line="">top</code> to an entry; it&#8217;s always there.</p>
<h3 id="schema-the-directorys-type-system">Schema: The Directory&#8217;s Type System</h3>
<p>Schema is the global rulebook for the entire directory. It defines:</p>
<ul>
<li><strong>Attribute type definitions</strong> — what each attribute is named, what syntax it uses (a string? an integer? a binary blob?), whether it&#8217;s case-sensitive, whether multiple values are allowed</li>
<li><strong>Object class definitions</strong> — which attributes each class requires or permits</li>
<li><strong>Matching rules</strong> — how equality comparisons work for each attribute type</li>
</ul>
<p>The schema is stored in the directory itself, under a special entry at <code class="" data-line="">cn=schema,cn=config</code> (OpenLDAP) or <code class="" data-line="">cn=Schema,cn=Configuration</code> (Active Directory). You can query it:</p>
<pre><code class="" data-line=""># View the schema for the posixAccount object class
ldapsearch -x -H ldap://your-dc \
  -b &quot;cn=schema,cn=config&quot; \
  &quot;(objectClass=olcObjectClasses)&quot; \
  olcObjectClasses | grep -A 10 &quot;posixAccount&quot;

# Output:
# olcObjectClasses: ( 1.3.6.1.1.1.2.0
#   NAME &#039;posixAccount&#039;
#   DESC &#039;Abstraction of an account with POSIX attributes&#039;
#   SUP top
#   AUXILIARY
#   MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
#   MAY ( userPassword $ loginShell $ gecos $ description ) )
</code></pre>
<p>That OID (<code class="" data-line="">1.3.6.1.1.1.2.0</code>) is the globally unique identifier for the <code class="" data-line="">posixAccount</code> object class. Every object class and attribute type in every LDAP directory on the planet has a unique OID assigned by an authority. This is how schema interoperability works across different directory implementations — OpenLDAP, Active Directory, and 389-DS can all understand each other&#8217;s <code class="" data-line="">posixAccount</code> entries because they share the same OID.</p>
<hr />
<h2 id="ldap-operations-what-actually-runs">LDAP Operations: What Actually Runs</h2>
<p>LDAP defines eight operations. Day-to-day authentication uses two: Bind and Search.</p>
<pre><code class="" data-line="">LDAP Operation Set
──────────────────
Bind        ← authenticate (prove identity)
Search      ← query the directory
Add         ← create a new entry
Modify      ← change attributes on an existing entry
Delete      ← remove an entry
ModifyDN    ← rename or move an entry
Compare     ← test if an attribute has a specific value
Abandon     ← cancel an outstanding operation
</code></pre>
<h3 id="bind-proving-who-you-are">Bind: Proving Who You Are</h3>
<p>Before any authenticated operation, the client sends a Bind request. There are two types:</p>
<p><strong>Simple Bind</strong> — the client sends its DN and password in the clear (or over TLS). This is what <code class="" data-line="">-x</code> in <code class="" data-line="">ldapsearch</code> means: simple authentication.</p>
<pre><code class="" data-line=""># Simple bind as a service account
ldapsearch -x \
  -D &quot;cn=svc-ldap-reader,ou=services,dc=corp,dc=com&quot; \
  -w &quot;service-account-password&quot; \
  -H ldap://dc.corp.com \
  -b &quot;dc=corp,dc=com&quot; \
  &quot;(uid=vamshi)&quot;
</code></pre>
<p><strong>SASL Bind</strong> — the client uses an authentication mechanism registered with SASL (Simple Authentication and Security Layer). Kerberos (via the GSSAPI mechanism) is the most common. EP05 covers Kerberos in detail.</p>
<pre><code class="" data-line=""># SASL bind using Kerberos (after kinit)
ldapsearch -Y GSSAPI \
  -H ldap://dc.corp.com \
  -b &quot;dc=corp,dc=com&quot; \
  &quot;(uid=vamshi)&quot;
</code></pre>
<p>An anonymous Bind (no DN, no password) is also valid for directories configured to allow anonymous reads. Many public LDAP directories (and some internal ones, misconfigured) allow this.</p>
<h3 id="search-the-core-operation">Search: The Core Operation</h3>
<p>A Search request has five required parameters:</p>
<pre><code class="" data-line="">baseObject   — where in the DIT to start (e.g., &quot;dc=corp,dc=com&quot;)
scope        — how deep to look
               base    = only the base entry itself
               one     = one level below base (immediate children)
               sub     = entire subtree below base (most common)
derefAliases — how to handle alias entries (usually derefAlways)
filter       — what to match (e.g., &quot;(uid=vamshi)&quot;)
attributes   — which attributes to return (empty = return all)
</code></pre>
<p>When SSSD authenticates a user login, it runs exactly two Search operations:</p>
<pre><code class="" data-line="">Search 1 — find the user&#039;s entry
  base:       dc=corp,dc=com
  scope:      sub
  filter:     (uid=vamshi)
  attributes: dn, uid, uidNumber, gidNumber, homeDirectory, loginShell

Search 2 — find the user&#039;s group memberships
  base:       dc=corp,dc=com
  scope:      sub
  filter:     (member=uid=vamshi,ou=engineers,dc=corp,dc=com)
  attributes: dn, cn, gidNumber
</code></pre>
<p>The first search locates the user entry and retrieves the POSIX attributes. The second finds all group entries that contain the user&#8217;s DN as a member. These two queries are the complete basis for a Linux login over LDAP.</p>
<h3 id="search-filters">Search Filters</h3>
<p>LDAP filters follow a prefix (Polish notation) syntax. Every filter is wrapped in parentheses:</p>
<pre><code class="" data-line=""># Simple equality
(uid=vamshi)

# Presence — entry has this attribute at all
(mail=*)

# Substring match
(cn=vam*)

# Comparison
(uidNumber&gt;=1000)

# Logical AND — both conditions must match
(&amp;(objectClass=posixAccount)(uid=vamshi))

# Logical OR — either condition matches
(|(uid=vamshi)(mail=vamshi@corp.com))

# Logical NOT
(!(uid=guest))

# Combined — posixAccount entries with UID &gt;= 1000 and no disabled flag
(&amp;(objectClass=posixAccount)(uidNumber&gt;=1000)(!(pwdAccountLockedTime=*)))
</code></pre>
<p>The <code class="" data-line="">&amp;</code> and <code class="" data-line="">|</code> operators take any number of operands. Filter syntax looks strange the first time but is unambiguous and compact — which matters when you&#8217;re encoding it into BER for the wire.</p>
<hr />
<h2 id="what-actually-travels-on-the-wire">What Actually Travels on the Wire</h2>
<p>Every LDAP message is encoded in BER (Basic Encoding Rules), a binary subset of ASN.1. LDAP is not a text protocol.</p>
<p>When you run <code class="" data-line="">ldapsearch</code>, the tool constructs a BER-encoded <code class="" data-line="">SearchRequest</code> message and sends it over TCP. The server responds with one or more <code class="" data-line="">SearchResultEntry</code> messages (one per matching entry), followed by a <code class="" data-line="">SearchResultDone</code>. All of these are BER.</p>
<p>BER uses a type-length-value (TLV) encoding:</p>
<pre><code class="" data-line="">Tag byte(s)    — what type of data this is
Length byte(s) — how many bytes of data follow
Value byte(s)  — the actual data
</code></pre>
<p>A minimal LDAP SearchRequest for <code class="" data-line="">ldapsearch -x -b &quot;dc=corp,dc=com&quot; &quot;(uid=vamshi)&quot; uid</code> looks like this on the wire:</p>
<pre><code class="" data-line="">30 45          ← SEQUENCE (LDAPMessage)
  02 01 01     ← INTEGER 1 (messageID = 1)
  63 40        ← [APPLICATION 3] SearchRequest
    04 11       ← OCTET STRING: baseObject
      64 63 3d  ← &quot;dc=corp,dc=com&quot; (20 bytes)
      63 6f 72
      70 2c 64
      63 3d 63
      6f 6d
    0a 01 02   ← ENUMERATED: scope = wholeSubtree (2)
    0a 01 03   ← ENUMERATED: derefAliases = derefAlways (3)
    02 01 00   ← INTEGER: sizeLimit = 0 (unlimited)
    02 01 00   ← INTEGER: timeLimit = 0 (unlimited)
    01 01 00   ← BOOLEAN: typesOnly = false
    a7 0f      ← [7] equalityMatch filter
      04 03 75 69 64   ← attributeDesc: &quot;uid&quot;
      04 06 76 61 6d   ← assertionValue: &quot;vamshi&quot;
             73 68 69
    30 05      ← SEQUENCE: AttributeDescriptionList
      04 03 75 69 64   ← &quot;uid&quot;
</code></pre>
<p>You don&#8217;t need to read BER by hand in practice. But knowing it&#8217;s binary — not HTTP, not JSON, not plain text — explains some things:</p>
<ul>
<li>Why <code class="" data-line="">tcpdump port 389</code> shows binary output you can&#8217;t read directly</li>
<li>Why LDAP on port 389 looks different in Wireshark than HTTP traffic</li>
<li>Why <code class="" data-line="">ldapsearch</code> output (LDIF) is a transformation of the wire data, not the wire data itself</li>
</ul>
<p>To see the wire protocol in action:</p>
<pre><code class="" data-line=""># Run ldapsearch with debug output (level 1 = protocol tracing)
ldapsearch -d 1 -x \
  -H ldap://ldap.forumsys.com \
  -b &quot;dc=example,dc=com&quot; \
  -D &quot;cn=read-only-admin,dc=example,dc=com&quot; \
  -w readonly \
  &quot;(uid=tesla)&quot; cn

# You&#039;ll see output like:
# ldap_connect_to_host: TCP ldap.forumsys.com:389
# ldap_new_connection 1 1 0
# ldap_connect_to_host: Trying ldap.forumsys.com:389
# ldap_pvt_connect: fd: 5 tm: -1 async: 0
# TLS: can&#039;t connect.
# ldap_open_defconn: successful
# ber_scanf fmt ({it) ber:     ← BER decoding of the response
# ber_scanf fmt ({) ber:
# ber_scanf fmt (W) ber:
# ...
</code></pre>
<p>The <code class="" data-line="">ber_scanf</code> lines are the BER decoder working through the server&#8217;s response. Each line represents one TLV element being read off the wire.</p>
<hr />
<h2 id="reading-ldapsearch-output-every-field">Reading ldapsearch Output: Every Field</h2>
<p><code class="" data-line="">ldapsearch</code> output is LDIF (LDAP Data Interchange Format), defined in RFC 2849. It&#8217;s the standard text serialization of LDAP entries.</p>
<pre><code class="" data-line="">ldapsearch -x \
  -H ldap://ldap.forumsys.com \
  -b &quot;dc=example,dc=com&quot; \
  -D &quot;cn=read-only-admin,dc=example,dc=com&quot; \
  -w readonly \
  &quot;(uid=tesla)&quot; \
  cn mail uid uidNumber objectClass
</code></pre>
<p>Output, annotated:</p>
<pre><code class="" data-line=""># extended LDIF
#
# LDAPv3                              ← protocol version confirmed
# base &lt;dc=example,dc=com&gt; with scope subtree
# filter: (uid=tesla)                 ← your search filter echoed back
# requesting: cn mail uid uidNumber objectClass
#

# tesla, example.com                  ← comment: CN, base DN
dn: uid=tesla,dc=example,dc=com      ← Distinguished Name — full path in the tree

objectClass: inetOrgPerson           ← structural class: person with org attrs
objectClass: organizationalPerson    ← auxiliary: adds telephoneNumber etc.
objectClass: person                  ← auxiliary: adds sn (surname)
objectClass: top                     ← every entry has this implicitly
cn: Tesla                            ← common name (from inetOrgPerson MUST)
mail: tesla@ldap.forumsys.com        ← email (from inetOrgPerson MAY)
uid: tesla                           ← userid (from inetOrgPerson MAY)

# search result
search: 2                            ← messageID of the SearchResultDone
result: 0 Success                    ← 0 = no error; 32 = no such object; 49 = invalid credentials

# numResponses: 2                    ← 1 result entry + 1 SearchResultDone
# numEntries: 1
</code></pre>
<p>The <code class="" data-line="">result:</code> line is the one to watch when debugging. LDAP result codes:</p>
<table>
<thead>
<tr>
<th>Code</th>
<th>Meaning</th>
<th>What it tells you</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Success</td>
<td>Query ran, results returned (or no results found — check numEntries)</td>
</tr>
<tr>
<td>32</td>
<td>No Such Object</td>
<td>Base DN doesn&#8217;t exist in this directory</td>
</tr>
<tr>
<td>49</td>
<td>Invalid Credentials</td>
<td>Bind failed — wrong DN, wrong password, or account locked</td>
</tr>
<tr>
<td>50</td>
<td>Insufficient Access</td>
<td>Your bind DN doesn&#8217;t have read permission on these entries</td>
</tr>
<tr>
<td>53</td>
<td>Unwilling to Perform</td>
<td>Server refused the operation (e.g., password policy, anonymous bind disabled)</td>
</tr>
<tr>
<td>65</td>
<td>Object Class Violation</td>
<td>Add/Modify would violate schema (missing MUST attribute, unrecognized object class)</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="ports-389-636-and-3268">Ports: 389, 636, and 3268</h2>
<pre><code class="" data-line="">Port 389   — LDAP (plaintext, or StartTLS in-session upgrade)
Port 636   — LDAPS (LDAP wrapped in TLS from the start)
Port 3268  — Active Directory Global Catalog (plain)
Port 3269  — Active Directory Global Catalog over TLS
</code></pre>
<p><strong>Port 389 vs 636:</strong> Both carry the same BER-encoded LDAP protocol. The difference is when TLS starts. On 636 (LDAPS), the TLS handshake happens before the first LDAP message. On 389 with StartTLS, the client sends a plaintext <code class="" data-line="">ExtendedRequest</code> with OID <code class="" data-line="">1.3.6.1.4.1.1466.20037</code> to initiate the TLS upgrade, then both sides continue over TLS. In production, use one or the other — never unencrypted port 389. Your credentials transit the wire on every Bind.</p>
<p><strong>Ports 3268/3269 — Active Directory Global Catalog:</strong> AD organizes domains into forests. Each domain controller holds the full LDAP tree for its own domain. The Global Catalog is a read-only, partial replica of every domain in the forest — just the most-queried attributes from every object. When an application needs to find a user across domains in the same forest (not just in one domain), it queries the Global Catalog on 3268/3269 instead of a domain-specific DC on 389/636.</p>
<pre><code class="" data-line="">Forest: corp.com
  ├── Domain: corp.com       → DC at port 389/636   (full copy of corp.com)
  ├── Domain: emea.corp.com  → DC at port 389/636   (full copy of emea.corp.com)
  └── Global Catalog        → GC at port 3268/3269  (partial copy of ALL domains)
</code></pre>
<p>If your SSSD or application is configured to use port 3268 instead of 389, it&#8217;s talking to the Global Catalog — useful for forest-wide user lookups, but missing some less-common attributes that aren&#8217;t replicated to the GC.</p>
<hr />
<h2 id="try-it-ldapsearch-against-your-own-directory">Try It: ldapsearch Against Your Own Directory</h2>
<p>If your Linux machine is joined to AD or connected to an LDAP directory, you can run these right now:</p>
<pre><code class="" data-line=""># 1. Confirm your SSSD knows where the LDAP server is
grep -E &quot;ldap_uri|ad_domain|krb5_server&quot; /etc/sssd/sssd.conf

# 2. Look up your own user entry
ldapsearch -x \
  -H ldap://$(grep ldap_uri /etc/sssd/sssd.conf | awk -F= &#039;{print $2}&#039; | tr -d &#039; &#039;) \
  -b &quot;dc=$(hostname -d | sed &#039;s/\./,dc=/g&#039;)&quot; \
  &quot;(uid=$(whoami))&quot; \
  dn objectClass uid uidNumber gidNumber homeDirectory loginShell

# 3. Find the groups you&#039;re in
ldapsearch -x \
  -H ldap://your-dc \
  -b &quot;dc=corp,dc=com&quot; \
  &quot;(member=$(ldapsearch -x ... &quot;(uid=$(whoami))&quot; dn | grep ^dn | cut -d&#039; &#039; -f2-))&quot; \
  cn gidNumber

# 4. Check what object classes your entry has
ldapsearch -x \
  -H ldap://your-dc \
  -b &quot;dc=corp,dc=com&quot; \
  &quot;(uid=$(whoami))&quot; \
  objectClass
</code></pre>
<p>On a machine joined to Active Directory, the <code class="" data-line="">ldap_uri</code> in sssd.conf is your domain controller&#8217;s address. On FreeIPA or OpenLDAP, it&#8217;s the directory server. The same <code class="" data-line="">ldapsearch</code> commands work against all of them — because they all speak LDAP v3.</p>
<hr />
<h2 id="common-misconceptions"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Misconceptions</h2>
<p><strong>&#8220;The DN is like a file path.&#8221;</strong> The analogy holds for reading it, but the DIT is not a file system. Entries don&#8217;t inherit permissions from parent containers the way files inherit from directories. Access control in LDAP is defined by ACLs on the server — not by position in the tree.</p>
<p><strong>&#8220;LDAP is case-sensitive.&#8221;</strong> It depends on the attribute. Most string attributes (like <code class="" data-line="">cn</code> and <code class="" data-line="">mail</code>) use case-insensitive matching by default — <code class="" data-line="">(cn=Vamshi)</code> and <code class="" data-line="">(cn=vamshi)</code> return the same results. But some attributes (like <code class="" data-line="">userPassword</code> and most binary types) are case-sensitive. The schema&#8217;s matching rules define this per-attribute.</p>
<p><strong>&#8220;You need the full DN to search for a user.&#8221;</strong> No. The <code class="" data-line="">Search</code> operation with a <code class="" data-line="">sub</code> scope searches the entire subtree below the base DN. You search with a filter like <code class="" data-line="">(uid=vamshi)</code> without knowing the full DN. The DN comes back in the result.</p>
<p><strong>&#8220;LDAP accounts and Linux accounts are the same thing.&#8221;</strong> An LDAP user entry becomes a Linux account only if the entry has a <code class="" data-line="">posixAccount</code> object class with the required POSIX attributes (<code class="" data-line="">uidNumber</code>, <code class="" data-line="">gidNumber</code>, <code class="" data-line="">homeDirectory</code>). An LDAP entry without <code class="" data-line="">posixAccount</code> can exist in the directory but <code class="" data-line="">getent passwd</code> will not return it.</p>
<p><strong>&#8220;The objectClass attribute can be changed freely.&#8221;</strong> Structural object classes cannot be changed after an entry is created — you&#8217;d have to delete and recreate the entry. Auxiliary classes can be added or removed. This is why correctly choosing the structural class at entry creation time matters.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Domain</th>
<th>Relevance</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP Domain 5: Identity and Access Management</td>
<td>DIT structure, DN addressing, object classes, and schema are the data model underpinning every enterprise identity store — understanding them is foundational to managing directory-based IAM</td>
</tr>
<tr>
<td>CISSP Domain 4: Communications and Network Security</td>
<td>BER on port 389 is unencrypted; LDAPS (port 636) or StartTLS is required for production — wire-level understanding informs the transport security decision</td>
</tr>
<tr>
<td>CISSP Domain 3: Security Architecture and Engineering</td>
<td>Schema design and DIT hierarchy are architectural decisions with security consequences: overly permissive schemas enable privilege escalation; flat DITs make access delegation harder</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The DIT is a hierarchical database — every entry has a unique DN that describes its path from leaf to root</li>
<li>Object classes define the schema rules for each entry: what attributes are required (<code class="" data-line="">MUST</code>) vs optional (<code class="" data-line="">MAY</code>), and what the entry fundamentally is</li>
<li>For a user to be usable for Linux logins, the directory entry needs the <code class="" data-line="">posixAccount</code> object class with <code class="" data-line="">uidNumber</code>, <code class="" data-line="">gidNumber</code>, and <code class="" data-line="">homeDirectory</code> populated</li>
<li>An LDAP login is two operations: a Bind (authenticate), then a Search (retrieve POSIX attributes and group memberships)</li>
<li>Everything on the wire is BER-encoded binary — <code class="" data-line="">ldapsearch</code> output is LDIF, a human-readable transformation of what the wire actually carries</li>
<li>LDAP result code 0 means success; 49 means bad credentials; 32 means the base DN doesn&#8217;t exist — these are the three you&#8217;ll debug most often</li>
</ul>
<hr />
<hr />
<p>Run <code class="" data-line="">ldapsearch</code> against your own directory and look at the object classes on your entry. Does it have <code class="" data-line="">posixAccount</code>? Does it have <code class="" data-line="">shadowAccount</code>? What attributes is your SSSD actually reading on every login — and what does it do when the LDAP server is unreachable? <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP02 showed what&#8217;s inside the directory: the tree structure, the schema, the operations, and the wire protocol. What it left open is how Linux actually uses this information to grant a login.</p>
<p>LDAP is not, by itself, an authentication protocol. The Bind operation can verify a password — but that&#8217;s a tiny piece of what happens when you SSH into a machine joined to Active Directory. The full login flow runs through PAM, NSS, and SSSD before LDAP ever gets queried. EP03 traces that path.</p>
<p><em>Next: <a href="/ldap-authentication-linux-pam-nss/">LDAP Authentication on Linux: PAM, NSS, and the Login Stack</a></em></p>
<p>Get EP03 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&amp;linkname=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fldap-internals-directory-structure%2F&#038;title=LDAP%20Internals%3A%20The%20Directory%20Tree%2C%20Schema%2C%20and%20What%20Travels%20on%20the%20Wire" data-a2a-url="https://linuxcent.com/ldap-internals-directory-structure/" data-a2a-title="LDAP Internals: The Directory Tree, Schema, and What Travels on the Wire"></a></p><p>The post <a href="https://linuxcent.com/ldap-internals-directory-structure/">LDAP Internals: The Directory Tree, Schema, and What Travels on the Wire</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/ldap-internals-directory-structure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1771</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-09-01 04:19:34 by W3 Total Cache
-->