# Aegis — AI/LLM reference

> Aegis is a self-hosted enterprise LLM gateway: it fronts commercial and on-prem model providers behind an OpenAI/Anthropic-compatible API, enforces DLP (prompt-injection and data-classification blocking), records every request to an audit log, and applies per-user quotas and cost tracking.

This document is written for coding agents/LLMs. Every command and endpoint below was verified against Aegis v1.0.0 running locally on 2026-08-01. Version 1.0.0 is the first release; treat it as early-stage — the wire API is stable but has not been through an external security audit.

## Install / run

Aegis is a single Spring Boot application (Java 21). It requires PostgreSQL 16. Under the `prod` profile it also requires Redis, Kafka, and the on-prem Qwen classifier; under `local`/`dev` those are optional. An OIDC provider is always optional.

**Profile gotcha (important):** the default active profile is `local`, which hard-codes dev ports (`8084`) and a dev datasource (`localhost:5433`) and ignores `AEGIS_DB_URL`. For any real deployment you **must** set `SPRING_PROFILES_ACTIVE=prod`. The Docker image `EXPOSE`s `8080`, which is the port only under `prod`; started without a profile it binds `8084` and tries `localhost:5433`.

### Docker (recommended)

```bash
docker run -d --name aegis \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e AEGIS_DB_URL=jdbc:postgresql://<db-host>:5432/aegis \
  -e AEGIS_DB_USERNAME=aegis \
  -e AEGIS_DB_PASSWORD=<db-password> \
  -e AEGIS_REDIS_HOST=<redis-host> \
  -e AEGIS_KAFKA_BOOTSTRAP_SERVERS=<kafka-host:9092> \
  -e AEGIS_QWEN_BASE_URL=http://<qwen-host>:8000 \
  -e AEGIS_ENCRYPTION_KEY=$(openssl rand -base64 32) \
  -e AEGIS_JWT_SECRET=$(openssl rand -base64 32) \
  -e AEGIS_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e AEGIS_BOOTSTRAP_ADMIN_PASSWORD=<choose-a-strong-password> \
  iwasoftcom/aegis:1.0.0
```

- Image: `iwasoftcom/aegis:1.0.0` (and `:latest`) on Docker Hub. ~171 MB.
- Under `prod`, container listens on `8080` (`EXPOSE 8080`). Non-root user `aegis`.
- Healthcheck / liveness probe: `GET /actuator/health/liveness` → `{"status":"UP"}`. Note `GET /actuator/health` reports `DOWN` until Redis/Kafka readiness checks pass — use the `liveness` group for "is it up".
- The image runs Flyway migrations automatically on startup against the configured database.
- Provider keys under `prod` may be supplied either as env (`AEGIS_OPENAI_API_KEY`, `AEGIS_CLAUDE_API_KEY`, `AEGIS_DEEPSEEK_API_KEY`, `AEGIS_GEMINI_API_KEY`) or stored per-provider via the admin API (encrypted at rest — see Admin surface). Env keys are the simplest path; the admin store allows rotation and labels without a restart.

### Linux packages

- Debian/Ubuntu: `aegis_1.0.0_amd64.deb` — installs to `/opt/aegis`, provides a `systemd` unit named `aegis` and an `aegis` launcher on `PATH`. Depends on `openjdk-21-jre-headless`.
- RHEL/Fedora: `aegis-1.0.0.x86_64.rpm` — same layout. Requires `java-21-openjdk-headless`.
- Windows x64: `aegis-1.0.0-windows-x64.zip` — portable, contains `aegis.jar` + `aegis-baslat.bat`. Requires a Java 21 JRE on `PATH`.

All downloads: https://iwasoft.com (Products → Aegis → Download). Source is closed; not distributed.

### From the jar directly

```bash
java -jar aegis-app-1.0.0.jar
```

The default profile is `local`, which binds port `8084` and expects PostgreSQL at `localhost:5433`. Set `SPRING_PROFILES_ACTIVE=prod` for production (port `8080`, datasource from `AEGIS_DB_URL`).

## Configuration

All variables are environment variables.

Datasource keys are **profile-dependent**: under `prod` the datasource comes from `AEGIS_DB_URL` / `AEGIS_DB_USERNAME` / `AEGIS_DB_PASSWORD`; under `local` it is `localhost:5433` with `AEGIS_DB_PASSWORD` (URL hard-coded). Standard `SPRING_DATASOURCE_*` keys are honored only where the profile does not already bind the datasource.

| Variable | Default | Meaning |
|---|---|---|
| `SPRING_PROFILES_ACTIVE` | `local` | Active profile: `local`, `dev`, or `prod`. `local`/`dev` hard-code dev ports and datasource; `prod` reads everything from env and requires Redis/Kafka/Qwen. |
| `AEGIS_SERVER_PORT` | `8080` (`8084` under `local`/`dev`) | HTTP listen port. |
| `AEGIS_DB_URL` / `AEGIS_DB_USERNAME` / `AEGIS_DB_PASSWORD` | *(none; `prod`)* | PostgreSQL JDBC URL / user / password. Required under `prod`. |
| `AEGIS_REDIS_HOST` / `AEGIS_REDIS_PORT` (`6379`) / `AEGIS_REDIS_PASSWORD` | *(none; `prod`)* | Redis connection. Required under `prod` (DLP cache is forced to `redis` there). |
| `AEGIS_KAFKA_BOOTSTRAP_SERVERS` | *(none; `prod`)* | Kafka bootstrap servers. Required under `prod`. |
| `AEGIS_OPENAI_API_KEY` / `AEGIS_CLAUDE_API_KEY` / `AEGIS_DEEPSEEK_API_KEY` / `AEGIS_GEMINI_API_KEY` | *(none; `prod`)* | Upstream provider keys via env (alternative to the encrypted admin store). |
| `AEGIS_ENCRYPTION_KEY` | *(none)* | **Required in production.** AES-256-GCM master key for provider credentials at rest. Must decode to exactly 32 bytes (`openssl rand -base64 32`). If unset, an ephemeral key is generated per process and stored credentials become undecryptable across restarts. |
| `AEGIS_JWT_SECRET` | *(none)* | HS256 key Aegis signs its own tokens with (min 32 bytes). If unset, a random key is generated each start — sessions drop on restart and you cannot run more than one replica. Set it in production. Ignored when an external OIDC issuer is configured. |
| `AEGIS_JWT_LIFETIME_MINUTES` | `480` | Access-token lifetime (8h). |
| `AEGIS_BOOTSTRAP_ADMIN_USERNAME` | *(none)* | Break-glass admin username. See Admin surface → bootstrap. |
| `AEGIS_BOOTSTRAP_ADMIN_PASSWORD` | *(none)* | Break-glass admin password. Active only when both username and password are set. Every use is logged at WARN. Remove after the directory is configured. |
| `AEGIS_OIDC_ISSUER_URI` | *(none)* | Corporate OIDC issuer URI. If set, Aegis validates JWTs against it and disables its own username/password login. If unset, Aegis authenticates against its configured LDAP/AD directory (or, with no directory, runs open in dev mode). |
| `AEGIS_AUDIT_KAFKA_ENABLED` | `false` | Publish audit events to Kafka. When `false`, audit is written to PostgreSQL only. |
| `AEGIS_AUDIT_TOPIC` | `aegis.audit` | Kafka topic for audit events. |
| `AEGIS_AUDIT_CONSUMER_GROUP` | `aegis-audit-consumer` | Kafka consumer group. |
| `AEGIS_AUDIT_RETENTION_MONTHS` | `12` | Audit retention window. |
| `AEGIS_AUDIT_RETENTION_ENABLED` | `false` | Enable the scheduled retention purge. |
| `AEGIS_AUDIT_RETENTION_CRON` | `0 0 3 1 * *` | Retention purge schedule (03:00 on the 1st). |
| `AEGIS_COST_PRICING_RESOURCE` | `classpath:cost/pricing.yml` | Per-model pricing table for cost tracking. |
| `AEGIS_BUDGET_MONITOR_ENABLED` | `false` | Enable scheduled budget monitoring. |
| `AEGIS_BUDGET_WARN` | `0.80` | Fraction of budget that triggers a warning. |
| `AEGIS_BUDGET_CRITICAL` | `1.00` | Fraction that triggers a critical alert. |
| `AEGIS_BUDGET_MONITOR_CRON` | `0 5 * * * *` | Budget-check schedule (hourly at :05). |
| `AEGIS_RATE_LIMITER_TYPE` | `in-memory` | `in-memory` or `redis`. Use `redis` for multi-replica rate limiting. |
| `AEGIS_DLP_CACHE_TYPE` | `in-memory` | `in-memory` or `redis`. Classification-result cache backend. |
| `AEGIS_DLP_CACHE_TTL_HOURS` | `24` | Classification cache TTL. |
| `AEGIS_ONPREM_FALLBACK_MODEL` | `Qwen3-32B-Instruct` | On-prem model used as fallback. |
| `AEGIS_QWEN_BASE_URL` | `http://localhost:8000` | Base URL of the on-prem Qwen classifier/model (direct vLLM in dev; API gateway URL in prod). |
| `AEGIS_QWEN_MODEL` | `florence_v2` | Model id/alias used for DLP classification. |
| `AEGIS_QWEN_CHAT_PATH` | `/v1/chat/completions` | Chat path on the Qwen endpoint. |
| `AEGIS_QWEN_TIMEOUT_MS` | `120000` | Qwen call timeout. |
| `AEGIS_QWEN_GATEWAY_OAUTH_URL` / `_CLIENT_ID` / `_CLIENT_SECRET` / `_USERNAME` / `_PASSWORD` / `_SCOPE` (`security`) / `_LLM_API_KEY` | *(none)* | OAuth credentials for the on-prem model API gateway, when the Qwen endpoint is behind one. |

Fixed (not env-configurable) facts: DLP fail-safe classification level is `CONFIDENTIAL` (a classifier failure blocks, it does not pass through). Correlation header is `X-Correlation-Id`.

## API / interface quickstart

Aegis exposes an OpenAI-compatible and an Anthropic-compatible endpoint. Agents point their base URL at Aegis; Aegis runs DLP + policy, then injects the stored upstream provider key and forwards the call.

Auth: every API call except `/actuator/health*`, `/api/v1/auth/login`, and `/api/v1/auth/methods` requires `Authorization: Bearer <aegis-token>`. Obtain the token from the login endpoint. Aegis does **not** accept a raw provider key as the bearer — the provider key is stored server-side (see Admin surface) and injected upstream by Aegis.

### 1. Get a token

```bash
curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<password>"}'
```

Response:

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresInSeconds": 28800,
  "username": "admin",
  "displayName": "admin",
  "department": null,
  "roles": ["ADMIN"]
}
```

`GET /api/v1/auth/methods` (no auth) reports which login methods are active: `{"oidc":false,"ldap":true}`.

### 2. Chat completion (OpenAI-compatible)

```bash
curl -s -X POST http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
```

The `model` prefix selects the provider: `gpt`/`chatgpt`/`o1`/`o3`/`o4` → OpenAI, `claude`/`*anthropic*` → Anthropic, `deepseek` → DeepSeek, `gemini` → Gemini, `qwen` → on-prem Qwen. An explicit `provider/model` prefix also works. The model must be registered and `ACTIVE` in the model registry, and a provider credential must be stored, or the call is rejected before reaching the provider.

Anthropic-compatible endpoint: `POST /v1/messages` with the Anthropic Messages wire format (top-level `system`, `max_tokens`, `messages`).

### 3. Enforcement responses (real, verified)

Errors are JSON `{code, message, correlationId, timestamp, details}`. Enforcement happens before the upstream call:

Prompt-injection attempt → **blocked**:
```json
{"code":"AEG-100","message":"İçerik prompt enjeksiyonu / atlatma girişimi tespit edildi ve reddedildi.","details":{"classification":"CONFIDENTIAL"}}
```

Model not registered/whitelisted → **blocked**:
```json
{"code":"AEG-102","message":"Seçilen sağlayıcı veya model bu istek için kullanılamaz.","details":{"reason":"model not in whitelist","model":"claude-3-5-sonnet","provider":"CLAUDE"}}
```

Passed policy, no upstream credential configured → provider call fails:
```json
{"code":"AEG-200","message":"Sağlayıcıya çağrı başarısız: 401 Unauthorized","details":{"provider":"OPENAI"}}
```

Error codes: `AEG-001` validation · `AEG-002` auth required · `AEG-003` forbidden · `AEG-100` injection blocked · `AEG-102` provider/model unavailable · `AEG-103` rate limit · `AEG-104` monthly quota exhausted · `AEG-110` classifier could not evaluate safely · `AEG-200`/`AEG-201` provider error/timeout · `AEG-999` unexpected.

### 4. Internal chat portal API (optional)

Aegis also ships a first-party chat portal backed by `POST /api/v1/chat/sessions` (create), `GET /api/v1/chat/sessions` (list, paged), `GET|POST /api/v1/chat/sessions/{id}/messages`, `POST /api/v1/chat/sessions/{id}/provider-switch`. Create body: `{"title": "...", "provider": "OPENAI", "model": "gpt-4o-mini"}`.

## Admin / management surface

Admin API base: same host/port as above. Two web UIs ship separately (admin console and chat portal) but everything is reachable over the REST API below. All admin endpoints require a token with role `ADMIN`.

### First administrative credential (bootstrap flow)

There is no default admin password. Set `AEGIS_BOOTSTRAP_ADMIN_USERNAME` + `AEGIS_BOOTSTRAP_ADMIN_PASSWORD` on startup. This "break-glass" account authenticates independently of any directory and always has `ADMIN` — it is the only way in before LDAP/OIDC is configured, and the recovery path if the directory is later misconfigured. Every login with it is logged at WARN. Remove both variables once real identity (OIDC or LDAP) is configured. When an OIDC issuer is set, username/password login (including bootstrap) is disabled and `POST /api/v1/auth/login` returns `AEG-409`.

### Common tasks (exact calls)

Set `TOKEN` to an `ADMIN` token first.

Store an upstream provider API key (so the gateway can call OpenAI/Anthropic/etc.):
```bash
curl -X POST http://localhost:8080/api/v1/admin/provider-credentials \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"provider":"OPENAI","label":"prod-key","apiKey":"sk-..."}'
```
The key is encrypted at rest with `AEGIS_ENCRYPTION_KEY`; it is never returned in plaintext (reads show a masked form, e.g. `sk-***1234`). Providers: `OPENAI`, `CLAUDE`, `ANTHROPIC`, `DEEPSEEK`, `GEMINI`, `QWEN_ONPREM`. Manage with `GET`/`PUT /{id}`, `POST /{id}/activate`, `POST /{id}/deactivate`, `DELETE /{id}`.

List providers and their whitelisted models: `GET /api/v1/admin/providers` (read-only view derived from the model registry).

Inspect / manage the model registry (governs which models are callable):
```bash
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/models      # active models
curl -X POST http://localhost:8080/api/v1/models \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"id":"claude-3-5-sonnet","provider":"CLAUDE","displayName":"Claude 3.5 Sonnet","status":"ACTIVE","capabilities":["TEXT_GENERATION"],"weight":100}'
curl -X POST http://localhost:8080/api/v1/models/gpt-4o-mini/status \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"status":"DISABLED"}'
```
Model `status`: `ACTIVE`, `CANARY`, `DEPRECATED`, `DISABLED`. Only `ACTIVE`/`CANARY` are callable. Seeded models at first migrate: `gpt-4o-mini` (openai), `claude-sonnet-4` (anthropic), `deepseek-chat`, `gemini-1.5-pro`, `qwen3-32b` (on-prem).

Set the tenant company name (white-label branding):
```bash
curl -X PUT http://localhost:8080/api/v1/admin/organization \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"companyName":"Contoso A.Ş."}'
```
Read it (any signed-in user): `GET /api/v1/branding`.

Configure LDAP / Active Directory (the directory Aegis authenticates users against when no OIDC issuer is set):
```bash
curl -X PUT http://localhost:8080/api/v1/settings/ldap \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "enabled": true,
    "serverUrl": "ldap://dc.example.org:389",
    "baseDn": "dc=example,dc=org",
    "managerDn": "cn=svc,dc=example,dc=org",
    "managerPassword": "...",
    "userSearchBase": "ou=people,dc=example,dc=org",
    "userSearchFilter": "(sAMAccountName={0})",
    "groupSearchBase": "ou=groups,dc=example,dc=org",
    "groupMappings": [{"ldapGroup":"AEGIS-Admins","aegisRole":"ADMIN"}]
  }'
```
`GET /api/v1/settings/ldap` returns the config (password shown only as `hasPassword`, never plaintext). `POST /api/v1/settings/ldap/test` tests the bind without saving; `GET /api/v1/settings/ldap/groups` lists directory groups. Group→role mappings are sent inside the same PUT body (`groupMappings`), not a separate endpoint. Roles: `ADMIN`, `COMPLIANCE_OFFICER`, `DEPARTMENT_LEAD`, `END_USER`.

## Architecture facts that affect integration

- **Auth model.** Three mutually exclusive modes, chosen at runtime: (1) OIDC when `AEGIS_OIDC_ISSUER_URI` is set — Aegis is a resource server, tokens come from the corporate IdP; (2) directory login when LDAP/AD is configured and no OIDC — Aegis validates username/password against the directory, provisions the user into its `app_user` table, and issues its own HS256 JWT; (3) open dev mode when neither is configured. A token's `sub` is always the Aegis `app_user` UUID, not the login name. Roles are carried in the `aegis_roles` claim.
- **DLP is inline and fail-safe.** Every gateway request is classified before forwarding. Prompt-injection detection and data-classification run first; on classifier failure the request is treated as `CONFIDENTIAL` and blocked (`AEG-110`/`AEG-100`), never passed through. This is not optional per request.
- **Provider keys are server-side.** Callers never send provider keys. Keys are stored encrypted (AES-256-GCM, envelope-encrypted with `AEGIS_ENCRYPTION_KEY`) and injected at egress. Losing `AEGIS_ENCRYPTION_KEY` makes stored keys unrecoverable.
- **Multi-tenancy / branding is single-tenant-per-deployment.** One deployment serves one organization; the company name and directory are singletons. White-labeling is per-deployment configuration, not per-request.
- **Persistence.** PostgreSQL 16 is the source of truth (audit, sessions, credentials, registry, quotas). Audit and cost tables are partitioned. Redis and Kafka are optional under `local`/`dev` (the product runs on PostgreSQL alone), but the `prod` profile requires both and forces the DLP cache onto Redis.
- **Audit.** Every request/response and every policy block is recorded, including (when enabled by the operator) the blocked prompt content and source site for browser-extension blocks. Blocked-content visibility in the audit API is role-gated.
- **Statelessness.** Sessions are JWT-based (`SessionCreationPolicy.STATELESS`); horizontal scaling requires a fixed `AEGIS_JWT_SECRET` and `redis` for the rate limiter and DLP cache.

## Links

- Downloads (all platforms + Docker): https://iwasoft.com → Products → Aegis
- Docker Hub: https://hub.docker.com/r/iwasoftcom/aegis (`iwasoftcom/aegis:1.0.0`, `:latest`)
- Documentation (human, 7 languages): published alongside this file under Products → Aegis → Documentation
- Implements: the OpenAI Chat Completions API (`/v1/chat/completions`) and the Anthropic Messages API (`/v1/messages`) as compatibility surfaces.
