Active Directory REST API for querying and managing users, groups, and OUs over LDAP.
- SDK used in this workspace: .NET
10.0.101 - Application target framework:
net10.0 - Test target framework:
net10.0
- Exposes versioned REST endpoints for:
- Users
- Groups
- Organizational Units (OUs)
- Performs LDAP-backed read/write operations
- Uses API-key based auth + claims authorization (
isAdministrator,isMonitor) - Publishes OpenAPI/Swagger docs
adrapi/: Web API host, controllers, LDAP integration, security middleware, managersdomain/: Domain models and custom exceptionstests/: Test project and environment-specific test settingsbuild/: NUKE build tooling
Main runtime configuration lives in:
adrapi/appsettings.jsonadrapi/appsettings.Development.json(gitignored — local overrides only, no secrets)adrapi/security.json
Configuration sources are layered in this order (later wins):
appsettings.jsonappsettings.{Environment}.json- User Secrets (Development environment only — see below)
- Environment variables
- Command-line arguments
Important settings:
ldap: LDAP servers and connection detailscertificate:file/certificate:password: HTTPS certificate for KestrelAllowedHosts: Host binding behavior (*maps to0.0.0.0in startup)
LDAP bind credentials, the HTTPS certificate password, and any other secret value
must not be written to appsettings.Development.json. Use the
.NET Secret Manager
instead. Secrets are stored outside the repo:
- Linux/macOS:
~/.microsoft/usersecrets/<UserSecretsId>/secrets.json - Windows:
%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json
The project is already initialized (UserSecretsId in adrapi/adrapi.csproj). To
populate your local store, from adrapi/:
dotnet user-secrets set "ldap:bindDn" "cn=<service-account>,...,dc=fgv,dc=br"
dotnet user-secrets set "ldap:bindCredentials" "<password>"
dotnet user-secrets set "certificate:password" "<cert-password>"
# Inspect / clear:
dotnet user-secrets list
dotnet user-secrets remove "ldap:bindCredentials"
dotnet user-secrets clearUser Secrets are loaded automatically when ASPNETCORE_ENVIRONMENT=Development
(the default for DEBUG builds). They override values in the JSON files.
For staging/production, supply the same keys via environment variables (e.g.
LDAP__BINDCREDENTIALS) or a secret manager (Azure Key Vault, AWS Secrets
Manager, HashiCorp Vault) — never via committed JSON.
The API rejects any LDAPS server certificate that does not chain to a trusted root in the OS store, unless its SHA-256 thumbprint has been explicitly added to the pin store for that host. There is no insecure "accept any certificate" fallback.
- Pin store file — defaults to
cfg/ldap-trusted-certs.json(override with the config keyldap:trustedCertificatesFile). The file is gitignored. - What gets pinned — the SHA-256 of the DER-encoded server certificate, scoped to a hostname. Multiple pins per host are allowed (use during rotation: add the new pin before the old cert is replaced, remove the old one afterwards).
- What happens on rejection — the LDAP bind fails and NLog records a single
error line with
SslPolicyErrors,sha256,subject,issuer, and the exact command to authorize the certificate.
To authorize a new LDAPS server (one-time per certificate), use the helper tool:
# Inspect the certificate and add it to the pin store after operator confirmation
dotnet run --project tools/AdrapiLdapCertPin -- sdcdc1vpr0006.fgv.br:636
# Non-interactive (e.g. for provisioning scripts)
dotnet run --project tools/AdrapiLdapCertPin -- sdcdc1vpr0006.fgv.br:636 --yes \
--note "Initial pin after DC certificate renewal 2026-05"
# List currently trusted pins
dotnet run --project tools/AdrapiLdapCertPin -- --list
# Remove a pin (e.g. after rotation)
dotnet run --project tools/AdrapiLdapCertPin -- --remove sdcdc1vpr0006.fgv.br ab:cd:ef:...
# Use a different store file (e.g. for a container deployment)
dotnet run --project tools/AdrapiLdapCertPin -- dc.example.com:636 --store /etc/adrapi/ldap-pins.jsonThe tool connects to the LDAPS endpoint, prints the certificate's subject /
issuer / validity / SHA-256 thumbprint, and writes the pin only after you
confirm. Always verify the thumbprint out-of-band (e.g. against the value
reported by the AD server admin) before answering y.
The API expects these headers:
api-key:keyID:secretKeyapi-version: API version selector
Claims are loaded from security.json.
isAdministrator: read/write accessisMonitor: read-oriented access
API keys live in a local SQLite database (default: cfg/api-keys.db,
configurable via security:databaseFile). Only an Argon2id hash of each secret
is persisted — plaintext is never stored. The database file is gitignored.
The store is managed with the adrapi-api-keys CLI tool. From the repo root:
# Create a new key (the plaintext secret is printed ONCE — record it now)
dotnet run --project tools/AdrapiApiKeys -- add \
--keyID admin-prod --ip 10.0.0.5 --claims isAdministrator
# List keys (metadata only — never the secret)
dotnet run --project tools/AdrapiApiKeys -- list
# Rotate a secret (issues a new plaintext; the old one stops working immediately)
dotnet run --project tools/AdrapiApiKeys -- rotate --keyID admin-prod
# Delete a key
dotnet run --project tools/AdrapiApiKeys -- remove --keyID admin-prod
# Debug: confirm a secret matches the stored hash
dotnet run --project tools/AdrapiApiKeys -- verify --keyID admin-prod --secret '...'
# Import an older plaintext security.json into the store (one-shot)
dotnet run --project tools/AdrapiApiKeys -- import --from /path/to/security.json
# All commands accept --db <path> to target a non-default store location.Migration from security.json: on first boot the API auto-imports any
security.json it finds next to the working directory, hashes each secret with
Argon2id, writes them into the SQLite store, and renames the source to
security.json.imported.<timestamp> so it isn't re-read. Review and delete the
archived file once you've verified the new store works.
Hash parameters (Argon2id, OWASP 2024 recommendation): memory 19 MiB, iterations 2, parallelism 1, salt 16 B, output 32 B. Each verify costs roughly 20–50 ms on commodity hardware — acceptable per-request for admin APIs, and a strong barrier against offline cracking if the DB ever leaks.
Production deployment: keep the .db file on encrypted storage, restrict
filesystem permissions to the API process user, and back it up out-of-band. For
container deployments mount it from a persistent volume (or generate it at
deploy time from a secret-manager-sourced seed via the CLI).
Test keys live in an ephemeral SQLite file created by each test fixture
(tests/Security.cs); nothing about test data is persisted.
Sensitive config values — ldap:bindDn, ldap:bindCredentials,
certificate:password — are stored encrypted in the same SQLite database as
the API keys (table app_secrets). On startup the API reads and decrypts them
into the standard IConfiguration tree, so existing code (LdapConfig,
certificate:*) keeps working without changes.
Encryption. ChaCha20-Poly1305 (256-bit key, AEAD, RFC 8439 — quantum-safe in practice: Grover's algorithm only halves effective key strength to 128 bits, still secure). The 256-bit key is derived via HKDF-SHA256 from:
- the machine ID (
/etc/machine-idon Linux,ioreg IOPlatformUUIDon macOS,HKLM\…\MachineGuidon Windows; falls back to hostname+OS), and - a random 32-byte seed generated on first run of the CLI tool and stored
at
cfg/.seed(0600 on Unix), configurable viasecurity:seedFile.
Both factors are required to decrypt: an attacker with the SQLite file alone sees nothing useful; with seed + DB but on a different host, decryption still fails. Conversely, if you migrate hosts or lose the seed, the data is unrecoverable — back up the seed file out-of-band.
Managing secrets (same CLI tool, secret subcommand group):
# Set a value interactively (input hidden) or with --value
dotnet run --project tools/AdrapiApiKeys -- secret set --name "ldap:bindCredentials"
# List secret names (values stay encrypted)
dotnet run --project tools/AdrapiApiKeys -- secret list
# Decrypt and print
dotnet run --project tools/AdrapiApiKeys -- secret get --name "ldap:bindCredentials"
# Delete
dotnet run --project tools/AdrapiApiKeys -- secret remove --name "ldap:bindCredentials"
# One-shot migration from appsettings/user-secrets JSON
dotnet run --project tools/AdrapiApiKeys -- secret import-ldap \
--from ~/.microsoft/usersecrets/<UserSecretsId>/secrets.jsonOn the very first secret command the tool generates and announces the seed
file with a prominent banner. Back it up.
POST /api/users/{userId}/authenticate and POST /api/users/authenticate are
protected by the Reading authorization policy (a valid api-key header is
required) and throttled by an ASP.NET Core rate limiter to blunt brute force:
- Policy name:
AuthEndpoint - Algorithm: sliding window
- Partition key:
ip + authenticated keyID— each unique (client IP, API keyID) pair gets its own bucket - Default: 5 requests / 60 seconds per partition
- Response on overflow:
429 Too Many RequestswithRetry-Afterheader; also logged as a structured warning (path,ip,keyID)
Tune via rateLimit:auth in appsettings.json:
"rateLimit": {
"auth": {
"permitLimit": 5,
"windowSeconds": 60,
"segmentsPerWindow": 6
}
}Behind a reverse proxy: the limiter uses
HttpContext.Connection.RemoteIpAddress. If the API is fronted by nginx/HAProxy,
enable forwarded-headers middleware (UseForwardedHeaders) so the real client
IP is used as the partition key — otherwise every request appears to come from
the proxy and shares one bucket.
dotnet restore adrapi.sln
dotnet run --project adrapi/adrapi.csprojDefault Kestrel bindings are configured in code:
- HTTP:
:6000 - HTTPS:
:6001
- Interactive docs:
/swagger - Detailed reference:
adrapi/docs/API_REFERENCE.md - Usage guide:
adrapi/docs/USAGE_GUIDE.md - CLI tools (
adrapi-api-keys):adrapi/docs/CLI_TOOLS.md - Migration notes:
adrapi/docs/MIGRATION_NOTES.md - Curl collection:
adrapi/docs/CURL_COLLECTION.md
The docs/ folder is also a docsify site. Browse it
locally with:
./serve-docs.sh # macOS/Linux -> http://localhost:3000
serve-docs.bat # Windows- Use a local
net10.0runtime/SDK for build and tests. - The repository includes legacy code paths and compatibility behaviors for older LDAP contracts; keep changes backward-compatible unless intentionally versioned.
Apache License v2.0