Installation

Three ways to get a running instance, from quickest to most real:

  1. Docker quick start: API, console and a demo OpenLDAP, one command. For trying it out.
  2. From source, against your directory: the API and console run from the working tree, pointed at an AD or LDAP server you already have.
  3. 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. StandardLdap talks 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:Key once 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.

  1. Steps 1–3 of path 2 (code, configuration files, keys).
  2. Start the directory, and the database beside it:

    docker compose -f deploy/dev/docker-compose.ldap.yml up -d --wait
    

    On Linux, if your user is not in the docker group, prefix it with sudo. The first start loads all the data and takes about 40 seconds; --wait returns when it is done.

  3. 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: None is 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.

  4. Steps 7–8 of path 2. Sign in as admin.demo / Axia-Demo-1.

Checking that it works

  • http://localhost:5063/swagger loads (Development only).
  • POST /api/v1/auth/login with a directory account answers 200 with 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 email but 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.