Installation
Three ways to get a running instance, from quickest to most real:
- Docker quick start: API, console and a demo OpenLDAP, one command. For trying it out.
- From source, against your directory: the API and console run from the working tree, pointed at an AD or LDAP server you already have.
- From source, against the local test directory: the same, against a throwaway AD-shaped OpenLDAP in Docker. For development.
Production is a separate page: Production deployment.
Requirements
What you need, by path
| Quick start | From source | Local test directory | |
|---|---|---|---|
| Docker Engine with Compose v2 | yes | — | yes |
| .NET SDK 10 | — | yes | yes |
| Node.js 22 (or 20.19+) and npm | — | yes | yes |
| An LDAP / AD server you can reach | — | yes | — (provided) |
| PostgreSQL | — (provided) | yes, or SQLite, or the one in deploy/dev | — (provided) |
OpenLDAP client tools (ldapsearch, ldapadd) | — | optional | optional, for poking at it |
| Python 3 | — | — | only to regenerate test data |
openssl | — | to generate keys | to generate keys |
Versions the project is built and tested with:
| Dependency | Version | Notes |
|---|---|---|
| .NET SDK | 10.0 (10.0.112) | global.json is not pinned; any 10.0 SDK should do. |
| ASP.NET Core runtime | 10.0 | For running a published build without the SDK. |
| Node.js | 22 (20.19+ works) | Vite 8 needs 20.19 or 22.12 and later. CI and the console’s Docker image build on Node 22. |
| Docker / Compose | Compose v2 (docker compose, not docker-compose) | |
| OpenLDAP image (quick start, dev) | osixia/openldap:1.5.0 | |
| PostgreSQL | 17 (postgres:17-alpine in the quick start, dev and CI) | Any supported version should do. |
| nginx image (console container) | nginxinc/nginx-unprivileged:1.27-alpine |
Operating system
- Linux, macOS, Windows: all fine for the API with
ProviderType=StandardLdap. - Windows only:
ProviderType=ActiveDirectory. On any other OS the API refuses to start with it.StandardLdaptalks to Active Directory just as well and is the recommended choice.
Libraries the API depends on
Restored automatically by dotnet restore; listed so you know what you are running.
| Package | Used for |
|---|---|
Novell.Directory.Ldap.NETStandard 3.6 | LDAP for the StandardLdap provider |
System.DirectoryServices (+ .AccountManagement) 10.0 | the Windows-only ActiveDirectory provider |
Npgsql.EntityFrameworkCore.PostgreSQL 10.0 | the database, on PostgreSQL |
Microsoft.EntityFrameworkCore.Sqlite 10.0 | the database, on SQLite |
Microsoft.AspNetCore.Authentication.JwtBearer 10.0 | bearer tokens |
MediatR 12, FluentValidation 12 | use-case pipeline and validation |
Serilog.AspNetCore 10, Serilog.Sinks.Http, Serilog.Sinks.Grafana.Loki | structured logging and shipping |
Swashbuckle.AspNetCore 6.6 | Swagger UI (Development only) |
The console: React 19, TypeScript 7, Vite 8. Nothing else at runtime.
Network
| From | To | Port | Why |
|---|---|---|---|
| Browser | console | 5500 (dev) / 8080 (container) | the UI |
| Browser | API | 5063 (dev) | the console calls the API directly in development; in the container setup, nginx proxies /api/ instead |
| API | directory | 636 (LDAPS, default), 389 (StartTLS or plain), 3268/3269 (Global Catalog, read-only) | every operation |
| API | PostgreSQL | 5432 | its own state; not with SQLite |
| API | Logstash / Loki | as configured | only if log shipping is turned on |
The API makes no other outbound connections.
Path 1: Docker quick start
Needs Docker with Compose v2 and nothing else.
git clone https://github.com/Nelsar/Axia.git
cd Axia
docker compose up
The first start pulls the released images of the API and the console from GitHub Container Registry, for amd64 and arm64. docker compose up --build builds them from the checkout instead, which takes a few minutes; it is also what happens when the version docker-compose.yml names has not been published. When the API logs Now listening on, open http://localhost:8080.
Every demo password is Axia-Demo-1:
| Login | Role |
|---|---|
admin.demo | Admin |
creator.demo | UserCreator |
editor.demo | UserEditor |
user.demo | User |
What docker compose up starts (see docker-compose.yml):
| Service | What it does |
|---|---|
init | Runs before the others. Generates a CA and a certificate for the directory, the API’s Jwt__Key and Encryption__Key, and the database’s password, into Docker volumes. Makes only what is missing, so they stay the same across restarts. |
openldap | OpenLDAP with the demo content from deploy/quickstart/bootstrap.ldif, LDAPS only, not published to the host. |
postgres | PostgreSQL 17, the API’s database, in the db-data volume. Not published to the host. |
api | The API: ghcr.io/nelsar/axia-api, or the working tree with --build. Reaches the directory over LDAPS and verifies its certificate. |
console | ghcr.io/nelsar/axia-console: nginx serving the built console on 127.0.0.1:8080, and proxying /api/ to the API. The only thing published. |
The port can be changed with CONSOLE_PORT=9090 docker compose up, and the images’ version with AXIA_VERSION=x.y.z docker compose up.
docker compose down stops it; docker compose down -v also deletes the directory, its certificates, the keys and the database.
A quick start set up by 0.2 or earlier kept its database in SQLite, in the api-data volume, which is no longer used; the new one starts empty. Moving to another database shows how to bring the old one along.
The quick start’s directory is reachable only inside Docker. To run the API from source against a directory on your machine, use path 3.
Path 2: From source, against your directory
1. Get the code and restore
git clone https://github.com/Nelsar/Axia.git
cd Axia
dotnet restore
npm --prefix frontend ci
2. Create the configuration files
The API reads Nelsar.Axia.Api/appsettings.json and, in Development, appsettings.Development.json on top of it. Neither is in the repository, because both hold secrets. Start from the examples:
cp Nelsar.Axia.Api/appsettings.json.example Nelsar.Axia.Api/appsettings.json
cp Nelsar.Axia.Api/appsettings.Development.json.example Nelsar.Axia.Api/appsettings.Development.json
3. Generate the two keys
openssl rand -base64 48 # Jwt:Key: token signing key, at least 32 bytes
openssl rand -base64 32 # Encryption:Key: AES key for stored bind passwords
Put the first under Jwt:Key and the second under Encryption:Key. The API refuses to start with either missing, too short, malformed, or still the placeholder from the example file.
Keep
Encryption:Keyonce it is in use. Bind passwords of directories stored in the database are encrypted with it; a different key makes every one of them unreadable until it is entered again.
4. Give it a database
The Development example points at the PostgreSQL in deploy/dev, which needs only Docker:
docker compose -f deploy/dev/docker-compose.ldap.yml up -d --wait postgres
For a server of your own, set ConnectionStrings:DefaultConnection in appsettings.Development.json to it. To run with no database server at all, use SQLite:
"Database": { "Provider": "Sqlite" },
"ConnectionStrings": { "DefaultConnection": "Data Source=nelsar-axia-dev.db" }
The API creates its tables on the first start. See Configuration.
5. Point it at your directory
Edit the Ldap section. For Active Directory over LDAPS:
"Ldap": {
"Server": "dc01.corp.example.com", // must match a name in the DC's certificate
"Security": "Ldaps", // the default; port 636
"BaseDn": "DC=corp,DC=example,DC=com",
"AdminUsername": "svc-axia@corp.example.com",
"AdminPassword": "…",
"ProviderType": "StandardLdap"
}
For OpenLDAP with StartTLS on 389:
"Ldap": {
"Server": "ldap.example.org",
"Security": "StartTls",
"BaseDn": "dc=example,dc=org",
"AdminUsername": "cn=axia,ou=services,dc=example,dc=org",
"AdminPassword": "…"
}
If the directory’s certificate is issued by an internal CA the machine does not trust, add "TrustedCaCertificatePath": "/path/to/ca.pem". Certificate verification cannot be turned off.
Every setting is described in the Configuration reference. What the bind account needs to be allowed to do is in Production → The bind account.
6. Say who the administrators are
The API will not start until some directory group, named by its full DN, is mapped to the Admin role:
"RoleMappings": {
"GroupDns": {
"Admin": "CN=Axia-Admins,OU=Groups,DC=corp,DC=example,DC=com",
"UserCreator": "CN=Axia-Creators,OU=Groups,DC=corp,DC=example,DC=com",
"UserEditor": "CN=Axia-Editors,OU=Groups,DC=corp,DC=example,DC=com",
"UserDeleter": "CN=Axia-Deleters,OU=Groups,DC=corp,DC=example,DC=com"
}
}
Only Admin is required. Use the group’s real DN; the API checks the format, not that the group exists. A wrong DN starts fine and leaves nobody able to administer it. See Troubleshooting.
On OpenLDAP, also list the groups whose members administer the directory itself, so only an application Admin can change them (AD marks these on its own):
"DirectoryProtection": { "ProtectedGroups": [ "cn=axia-admins,ou=groups,dc=example,dc=org" ] }
7. Run the API
dotnet run --project Nelsar.Axia.Api --launch-profile http
The http profile sets ASPNETCORE_ENVIRONMENT=Development and listens on http://localhost:5063. On first start the API creates its tables in the database, applies migrations and seeds defaults. Swagger UI is at http://localhost:5063/swagger in Development.
The https launch profile runs as Production: it does not load appsettings.Development.json and does not serve Swagger.
8. Run the console
npm --prefix frontend run dev
Open http://localhost:5500. The console calls the API at http://localhost:5063 by default; the sign-in screen has an API address field to change it. The API admits browsers on any loopback port without configuration. The field exists on the development server only: the Docker image’s console always calls the API on its own origin.
9. Sign in
Sign in with an account from your directory: a login (uid, sAMAccountName), a userPrincipalName, or an e-mail address, depending on Ldap:UserSearchFilter. Members of the group mapped to Admin get the Admin role. Everyone else gets User until more mappings are added.
Path 3: From source, against the local test directory
deploy/dev holds an OpenLDAP for development: published on 127.0.0.1:389 in plain LDAP, with the quick start’s demo accounts plus an Active-Directory-shaped domain of around 1,500 accounts, 2,000 computers and 110 groups — and a PostgreSQL on 127.0.0.1:5432 for the API’s own state. Local test directory describes what is in it.
- Steps 1–3 of path 2 (code, configuration files, keys).
-
Start the directory, and the database beside it:
docker compose -f deploy/dev/docker-compose.ldap.yml up -d --waitOn Linux, if your user is not in the
dockergroup, prefix it withsudo. The first start loads all the data and takes about 40 seconds;--waitreturns when it is done. -
In
appsettings.Development.json, point the API at it:"Ldap": { "Server": "localhost", "Port": 389, "Security": "None", "BaseDn": "dc=example,dc=org", "AdminUsername": "cn=admin,dc=example,dc=org", "AdminPassword": "adminpassword", "UserSearchFilter": "(&(objectClass=person)(|(uid={0})(sAMAccountName={0})(userPrincipalName={0})(mail={0})))", "ProviderType": "StandardLdap" }, "RoleMappings": { "GroupDns": { "Admin": "cn=axia-admins,ou=groups,dc=example,dc=org", "UserCreator": "cn=axia-creators,ou=groups,dc=example,dc=org", "UserEditor": "cn=axia-editors,ou=groups,dc=example,dc=org", "UserDeleter": "cn=axia-deleters,ou=groups,dc=example,dc=org" } }, "DirectoryProtection": { "ProtectedGroups": [ "cn=axia-admins,ou=groups,dc=example,dc=org" ] }Security: Noneis plain LDAP. It is acceptable here only because the directory is on the same machine; the API logs a warning about it on every connection. - Steps 7–8 of path 2. Sign in as
admin.demo/Axia-Demo-1.
Checking that it works
http://localhost:5063/swaggerloads (Development only).-
POST /api/v1/auth/loginwith a directory account answers200with a token:curl -s -X POST http://localhost:5063/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin.demo","password":"Axia-Demo-1"}'The field is called
emailbut takes any login the search filter matches. - The console’s dashboard shows the signed-in name and role.
If the API stops at startup, the last log line names the setting at fault. See Troubleshooting.