Building a Full-Stack Identity & Directory Service for an AI Operating System
Most platforms bolt identity on as an afterthought. A users table here, an API key there, maybe RBAC if you're lucky. When you're building an operating system that orchestrates 202 microservices and 29 AI agents, that approach falls apart on day one.
AitherOS needed something fundamentally different: a unified identity layer where every entity in the system — humans, AI agents, services, devices, secrets, certificates, policies — lives as a first-class principal in a single directory tree. Today we're pulling back the curtain on how we built it.
The Problem: Identity Sprawl
Picture a platform with dozens of microservices, each needing to know:
- Who is this user? (human or agent)
- What can they do? (RBAC with 50+ resource types)
- Which tenant do they belong to? (multi-tenant isolation)
- What services exist? (discovery, health, mTLS status)
- What secrets does this service need? (vault coordination)
- What certificates secure this connection? (PKI/TLS)
The conventional answer is to scatter this across a dozen databases and pray they stay in sync. We chose to put it all in one place.
AitherDirectory: One Tree to Rule Them All
AitherDirectory is an LDAP-compatible directory service backed by SQLite WAL, running at port 8214 as a Layer 0 infrastructure service. It boots before everything else and serves as the single source of truth for all identity objects.
The directory information tree (DIT) is rooted at dc=aither,dc=os and spans 13 organizational units:
dc=aither,dc=os
├── ou=users aitherUser Human principals
├── ou=agents aitherAgent AI agent identities
├── ou=groups aitherGroup Permission groups
├── ou=roles aitherRole Named permission sets
├── ou=tenants aitherTenant Multi-tenant containers
│ └── ou=users aitherUser Tenant-scoped users
├── ou=services aitherService Microservice entries
├── ou=devices aitherDevice Device identities
├── ou=mailboxes aitherMailbox Email configurations
├── ou=lockboxes aitherLockbox Encrypted storage metadata
├── ou=distribution-lists aitherDistributionList
├── ou=certificates aitherCertificate TLS/mTLS cert bindings
├── ou=secrets aitherSecret Secret metadata (never values)
└── ou=policies aitherPolicy Capability/access policies
Every entry gets an LDAP-style distinguished name. A user named "alice" in the "acme" tenant lives at uid=alice,ou=users,o=acme,ou=tenants,dc=aither,dc=os. An AI agent named "atlas" lives at cn=atlas,ou=agents,dc=aither,dc=os. A TLS certificate for the Genesis service is cn=genesis-tls,ou=certificates,dc=aither,dc=os.
This isn't a toy LDAP wrapper — it's a full directory store with three backing tables:
| Table | Purpose |
|---|---|
directory_entries | Flat DIT store: DN, RDN, parent DN, object class, JSON attributes |
directory_attr_index | Case-insensitive attribute index for LDAP filter matching |
directory_changelog | Mutation audit log: actor, change type, timestamp, old/new values |
13 Object Classes: Everything Is a Principal
The core schema defines 13 typed object classes with dedicated DN builders, serializers, and REST endpoints. Here's what makes each one interesting:
aitherUser — Humans and Legacy Accounts
Standard identity principal with bcrypt password hashes, SSH public keys, OAuth provider bindings (GitHub, Google), multi-tenant scoping, and persona references. Supports four user types: human, agent, service, and system.
aitherAgent — First-Class AI Citizenship
This is where things get unique. AI agents aren't shoehorned into the user table with a type=agent flag. They get their own object class with purpose-built attributes:
| Attribute | What it captures |
|---|---|
aitherAgentSkills | JSON array of skill descriptors |
aitherEffortCap | Maximum effort level (1–10 scale) |
aitherHourlyBudget / aitherDailyBudget | Token spend limits |
aitherA2AEndpoint | Google A2A protocol endpoint URL |
aitherA2AVersion | A2A protocol version |
aitherParentId / aitherChildren | Agent hierarchy (supervisor/worker) |
aitherToolProfile | Allowed tool categories |
aitherWorkTypes | Task types the agent handles |
aitherSpiritSnapshot | Personality/behavior state capture |
aitherPersonaId | Link to persona configuration |
Every agent has a DN, is LDAP-queryable, and participates in the same RBAC system as humans. When the Council agent dispatches work to Atlas, the directory knows Atlas's skill set, effort cap, and current budget — no separate agent registry needed.
aitherService — Microservice Identity
Each of our 202 microservices gets a directory entry with its port, layer assignment, health endpoint, Ed25519 identity key status, mTLS certificate serial, and vault secret inventory. Service discovery queries the directory, not a separate service mesh.
aitherSecret — Metadata Without the Danger
The secrets object class stores metadata only — name, type (API key, password, certificate, token), access level (public, internal, restricted, admin), scope (managed vs. unmanaged), and owning service. Actual secret values never enter the directory. They stay in the AitherSecrets vault (Fernet-encrypted at rest) and are referenced by DN.
aitherPolicy — Capabilities as Directory Objects
Access policies are stored as directory entries with subject, subject type (agent, service, role, user), resource, action, effect (allow/deny), conditions, HMAC-SHA256 capability tokens, and priority ordering. This means you can LDAP-query "which policies affect the atlas agent?" just like you'd query "which groups does alice belong to?"
aitherCertificate — PKI in the Tree
TLS and mTLS certificate bindings live as first-class entries. When a service renews its cert, the directory entry updates, and any service querying that cert binding sees the new serial number immediately.
RBAC: 50+ Resource Types with Hierarchical Inheritance
AitherRBAC is a 2,950-line RBAC engine that backs the identity system. The permission model is straightforward:
Permission = resource : action : scope
With 7 standard actions (create, read, update, delete, execute, admin, *) applied across 50+ resource types — from llm and canvas through voice, reasoning, orchestrator, atlas, chronicle, social, and service-level resources.
Roles support hierarchical inheritance with cycle detection. The admin role inherits from moderator, which inherits from editor, which inherits from readonly. The resolution algorithm walks the inheritance tree, merges permissions, and applies explicit denies last.
Groups add another layer — a user's effective permissions are the union of their direct role assignments, their group-inherited roles, and any explicit denies that override everything.
Enterprise Password Policy
The RBAC layer enforces a real password policy, not the "8 characters and a number" variety:
- 12-character minimum with uppercase, lowercase, digit, and special character requirements
- 90-day maximum age with 5-password history (no recycling)
- Account lockout after 10 failed attempts (30-minute cooldown)
- bcrypt hashing with configurable work factor
- All of this is runtime-configurable via the password policy API
The Bridge Architecture: Six Sync Pipelines
The directory is a coherent runtime cache, not the source of truth for everything. External systems remain authoritative for their domains, and six specialized bridges keep the directory in sync:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ agent_cards.yaml│ │ services.yaml │ │ AitherSecrets │
│ + identities/ │ │ (5,200 lines) │ │ :8111 (vault) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
AgentSync ServiceSync SecretsDirectoryBridge
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ AitherDirectory :8214 │
│ dc=aither,dc=os (unified DIT) │
└──────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
IdentityBridge SMTPBridge StrataBridge
│ │ │
┌────────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐
│ AitherRBAC │ │ AitherSMTP │ │ Strata/Lockbox │
│ (users/roles) │ │ (Proton Bridge)│ │ (encrypted FS) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
| Bridge | Source → Directory | What Syncs |
|---|---|---|
| AgentSync | YAML config → ou=agents | Skills, effort caps, A2A endpoints, hierarchy, tool profiles |
| ServiceSync | services.yaml → ou=services | Ports, layers, health endpoints, cert bindings |
| SecretsDirectoryBridge | Vault → ou=secrets + service entries | Secret metadata, identity status, mTLS status |
| IdentityDirectoryBridge | RBAC → ou=users, ou=groups, ou=roles | User profiles, group memberships, role definitions |
| SMTPDirectoryBridge | SMTP config → ou=mailboxes + ou=distribution-lists | Email configs, mailing lists |
| StrataDirectoryBridge | Strata → ou=lockboxes | Encrypted storage metadata, quotas, tiers |
Plus an external LDAP client for JIT user provisioning from enterprise Active Directory or OpenLDAP instances.
The VaultBootstrap Pipeline
Secrets don't just appear. The VaultBootstrap engine runs a 6-tier resolution chain for every secret declared in secrets_manifest.yaml:
- Vault check — already provisioned? Skip.
- Environment variable —
AITHER_INTERNAL_SECRET, etc. - Dotenv file —
.env,.env.cloudflare, service-specific.envfiles - Computed — derived from other secrets (e.g., HMAC keys from master secrets)
- Manifest default — fallback value declared in the manifest
- None — logged as a warning; service must handle missing secret gracefully
After resolution, the SecretsDirectoryBridge syncs metadata into the directory so any service can query "what secrets does the Genesis service have?" without touching the vault.
Dual Protocol Access: REST + LDAP
Every entry in the directory is accessible via two protocols:
REST API (port 8214) — Full CRUD plus typed endpoints for each object class. Want all agents with the "coding" skill? GET /directory/agents?skill=coding. All policies affecting the atlas agent? GET /directory/policies?subject=atlas&subject_type=agent. Secret inventory for the dashboard? GET /directory/secrets/inventory.
LDAP (port 8389) — A read-only LDAP server (asyncio/ldaptor) that exposes the entire DIT via standard LDAP protocol. External tools — IDE plugins, LDAP browsers, CI systems, even ldapsearch from the command line — can authenticate and query the directory using familiar LDAP filters:
ldapsearch -H ldap://localhost:8389 \
-b "ou=agents,dc=aither,dc=os" \
"(aitherAgentSkills=*coding*)"
Runtime-Extensible Schema
The schema isn't frozen at compile time. Any service can register new attributes on any object class at runtime via:
POST /directory/schema/extend
{
"object_class": "aitherService",
"attributes": [
{"name": "customMetric", "type": "string", "indexed": true}
]
}
The attribute index updates immediately, and LDAP queries can filter on the new attribute right away. This is how the SecretsDirectoryBridge adds aitherVaultSecrets and aitherMtlsStatus to service entries without modifying the core schema.
Changelog-Driven Audit Trail
Every mutation to the directory — create, modify, delete, rename — is logged in the changelog table with:
- Change ID (monotonic sequence)
- DN of the affected entry
- Change type (add, modify, delete, modrdn)
- Actor who made the change
- Old and new attribute values
- Timestamp
The changelog feeds into Strata (our telemetry/analytics layer) for compliance reporting, and it's queryable via GET /directory/changelog?dn=...&change_type=...&since=.... When something goes wrong at 3 AM, you can trace exactly who changed what and when.
Multi-Tenant Isolation
Tenant users live in a scoped subtree: uid=alice,ou=users,o=acme,ou=tenants,dc=aither,dc=os. LDAP subtree searches naturally scope to a tenant's branch, and the REST API enforces tenant boundaries at the query layer. A tenant admin can manage users within their o=acme subtree but can't see entries outside it.
What We Learned
Treat agents like principals, not rows. The moment we gave AI agents their own object class with typed attributes (skills, budgets, hierarchy), the rest of the system stopped treating them as second-class citizens. Agent dispatch queries the directory for capability matching instead of maintaining a separate agent registry.
Metadata belongs in the directory; values belong in the vault. The aitherSecret object class was a breakthrough. Services can discover what secrets exist and who owns them via LDAP without ever touching actual secret values. The security boundary is architectural, not just a policy.
Bridges beat bidirectional sync. We tried bidirectional sync early on and it was a nightmare of conflict resolution. The bridge pattern — external system is authoritative, directory is a read-optimized cache — eliminated an entire class of consistency bugs.
LDAP isn't dead, it just needed better company. The combination of REST for modern services and LDAP for legacy tooling means we never have to choose. An IDE plugin queries LDAP; a React dashboard queries REST. Same data, same tree, zero translation layer.
Changelog everything. The mutation log has paid for itself a hundred times over. When a service suddenly can't authenticate, the first question is always "did someone change the directory entry?" — and we can answer it in seconds.
The Numbers
| Metric | Value |
|---|---|
| Object classes | 13 core + 2 extension (mailbox, distribution list) |
| RBAC resource types | 50+ |
| Permission actions | 7 standard + wildcard |
| Default roles | 23 (hierarchical with inheritance) |
| Sync bridges | 6 specialized pipelines |
| REST endpoints | 40+ typed queries |
| LDAP port | 8389 (read-only) |
| Storage | SQLite WAL (zero-config, crash-safe) |
| Boot priority | Layer 0, Phase 0 (before all services) |
What's Next
We're working on cross-cluster directory federation — multiple AitherOS instances sharing a synchronized DIT subtree for agent delegation across deployment boundaries. The changelog-driven architecture makes this tractable: replicate the changelog, apply the deltas, resolve conflicts by timestamp and priority.
We're also exploring attribute-based access control (ABAC) layered on top of the existing RBAC, using the directory's rich attribute set for context-aware policy decisions. When the directory already knows an agent's skill set, effort cap, and current budget, the policy engine can make smarter dispatch decisions than pure role-based checks allow.
The identity layer is never "done" — but having a single, coherent directory tree as the foundation means every new capability builds on solid ground instead of adding another silo.
AitherOS is an AI operating system built by Aitherium. The identity and directory services described here are part of the open-source AitherZero automation framework and the AitherOS platform.