> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genesiscomputing.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying Genesis - Azure AKS with OIDC Authentication

> Set up Azure AD (Entra ID) authentication for Genesis on Azure Kubernetes Service using native OIDC - no OAuth2 Proxy required

This guide walks through setting up Azure AD authentication for Genesis deployed on Azure Kubernetes Service (AKS) using **native OIDC authentication** — no OAuth2 Proxy required.

## Overview

Genesis supports native OIDC authentication using Authorization Code Flow + PKCE. This approach:

* **No OAuth2 Proxy needed** — Genesis handles authentication directly
* **Simpler architecture** — Fewer moving parts to configure and maintain
* **Browser-native** — Works with all modern browsers including Safari and Chrome Incognito
* **Automatic token refresh** — Uses refresh tokens, not iframe-based silent renew
* **WebSocket support** — Authenticated real-time connections

### Architecture

```mermaid theme={null}
flowchart LR
  subgraph Client
    Browser["Browser (React)"]
  end
  subgraph Genesis
    Backend["Genesis Backend"]
  end
  subgraph Azure
    AD["Azure AD (Entra ID)"]
  end
  Browser -->|"Login / API"| Backend
  Backend -->|"Auth / validate tokens"| AD
  AD -->|"Tokens"| Backend
  Backend -->|"Responses"| Browser
```

1. User clicks login → redirected to Azure AD
2. User authenticates → returns with authorization code
3. Frontend exchanges code for Access Token + Refresh Token (PKCE flow)
4. API calls use `Authorization: Bearer <token>` header
5. Token refresh handled automatically (no user interaction)

## Prerequisites

* AKS cluster with NGINX ingress controller installed
* Azure CLI installed and authenticated
* kubectl configured to access your AKS cluster
* Helm 3.x installed
* Sufficient node resources (Standard\_D4s\_v3 or larger recommended)

### Hostname Configuration

<Note>
  **For Testing/Development:** This guide uses `nip.io` for quick testing (e.g., `genesis-oidc.172.214.4.208.nip.io`). `nip.io` is a free DNS service that maps any IP to a hostname. **Only use `nip.io` for testing and development.**

  **For Production:** You must create a permanent DNS name (e.g., `genesis-oidc.yourcompany.com`). Create a DNS A record pointing your domain to the ingress IP address, update all configuration files to use your permanent domain name, update Azure AD redirect URIs to use your permanent domain, and configure TLS certificates for HTTPS (required for production).
</Note>

## Step 1: Scale AKS Node Pool (if needed)

If you have resource constraints, ensure adequate capacity:

```bash theme={null}
# Check current nodes
kubectl get nodes

# Scale node pool if needed
az aks nodepool scale \
    --resource-group <resource-group> \
    --cluster-name <cluster-name> \
    --nodepool-name <nodepool-name> \
    --node-count 2 \
    --no-wait

# Verify nodes are ready
kubectl get nodes -w
```

## Step 2: Create Azure AD App Registration

### Using Azure Portal (Recommended)

1. Navigate to **Azure Portal** → **Microsoft Entra ID** → **App registrations**

2. Click **"New registration"**

3. Configure:
   * **Name**: Genesis (or your preferred name)
   * **Supported account types**: Single tenant (recommended)
   * **Redirect URI**: Leave empty for now

4. Click **"Register"**

5. **Copy these values** (you'll need them later):
   * **Application (client) ID** → This is your `AUTH0_CLIENT_ID`
   * **Directory (tenant) ID** → Used in `AUTH_ISSUER`

### Configure Authentication Platform

1. Go to **Authentication** blade (left sidebar)

2. Click **"Add a platform"**

3. Select **"Single-page application"** (NOT "Web"!)

4. Enter **Redirect URI**:
   * **For Testing**: `http://genesis-oidc.<INGRESS_IP>.nip.io` or `https://genesis-oidc.<INGRESS_IP>.nip.io`
   * **For Production**: `https://genesis-oidc.yourcompany.com` (your permanent domain)
   * **Important**: No trailing slash, no `/callback` path — just the origin

5. Click **"Configure"**

6. If hosted MCP for Claude Code will be enabled, click **"Add a platform"** again, select **"Mobile and desktop applications"**, and add this redirect URI:

   ```text theme={null}
   http://localhost:31318/callback
   ```

   This is Claude Code's local loopback callback. It does not require inbound network access to the user's laptop; after Microsoft sign-in, the user's browser redirects to localhost on the same machine.

7. Under **"Implicit grant and hybrid flows"**:
   * **Leave BOTH unchecked** (ID tokens AND Access tokens)
   * We use Authorization Code + PKCE, not Implicit flow

8. Click **"Save"**

### Configure Token Claims (Important)

Azure AD doesn't include email in tokens by default. To fix:

1. Go to **Token configuration** blade

2. Click **"Add optional claim"**

3. Select **ID** token type

4. Check: `email`, `upn`, `preferred_username`

5. Click **"Add"**

### Checklist

* [ ] Platform type is **"Single-page application"** (not "Web")
* [ ] Redirect URI is exact origin without trailing slash
* [ ] Hosted MCP public/native redirect URI is added: `http://localhost:31318/callback`
* [ ] Implicit grant checkboxes are **BOTH unchecked**
* [ ] Optional claims added: `email`, `upn`, `preferred_username`

## Step 3: Configure values-aks-oidc.yaml

Copy and edit the values file from your Genesis deployment bundle:

```bash theme={null}
cp Azure/values-aks-oidc.yaml Azure/my-values-oidc.yaml
```

### Required Configuration

Edit `Azure/my-values-oidc.yaml` with your values:

```yaml theme={null}
# Namespace
namespace: genesis-oidc

# Container image
image:
  repository: ghcr.io/genesis-computing-ai/genesis
  tag: v1.18.2
  pullPolicy: IfNotPresent

# Genesis AKS bundle uses ECR by default; registry/token are injected at deploy time
ecr:
  enabled: true
  registry: ""

# The LLM provider is not configured here. It is configured after the pod is
# running — see "Step 8: Configure the LLM Provider" below.
config:
  # OIDC Authentication (Native - no OAuth2 Proxy)
  authEnabled: "true"
  authProvider: "oidc"

  extraEnv:
    # Azure AD OIDC Configuration
    AUTH_ISSUER: "https://login.microsoftonline.com/<tenant-id>/v2.0"
    AUTH0_CLIENT_ID: "<application-client-id>"
    AUTH_AUDIENCE: "<application-client-id>"
    AUTH_JWKS_URL: "https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys"
    AUTH_ALLOW_QUERY_TOKEN: "true"
    # Optional: override the domain the web client uses for Auth0-style bootstrap.
    # For Entra ID with authProvider "oidc", set this when the issuer-derived
    # domain does not match the path structure the legacy Auth0 SDK expects.
    AUTH0_DOMAIN: "login.microsoftonline.com/<tenant-id>/oauth2/v2.0"

# Recommended for IaC-managed Genesis settings
genesisConfig:
  content: ""  # Set with --set-file genesisConfig.content=genesis-config.yaml
  existingConfigMap: ""  # Or reference a pre-created ConfigMap, not both

# Helm-driven RBAC (optional — see "Helm-driven RBAC" below)
groupRoleMapping:
  content: ""  # --set-file groupRoleMapping.content=group-role-mapping.yaml
rbacSeedPolicy:
  content: ""  # --set-file rbacSeedPolicy.content=rbac-policies.csv

# Optional extra Kubernetes resources rendered as separate YAML documents
extraManifests: []
```

<Note>
  If you prefer to version Genesis app settings separately from AKS values,
  move the OIDC, provider, and `embedding` configuration into
  `genesis-config.yaml` and keep
  Kubernetes settings such as `namespace`, `ingress`, and `resources` in
  `Azure/my-values-oidc.yaml`.

  ```yaml theme={null}
  auth:
    enabled: true
    provider: "oidc"
    issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
    audience: "<application-client-id>"
    jwks_url: "https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys"
    client_id: "<application-client-id>"
    allow_query_token: true
  ```

  Pass the file with `--set-file genesisConfig.content=genesis-config.yaml`, or
  use `genesisConfig.existingConfigMap` when the file already exists as a
  ConfigMap. Genesis mounts the file at
  `/app/.genesis/config/genesis-config.yaml`, and explicit environment
  variables still take precedence over file-backed values.
</Note>

<Tip>
  `extraManifests` defaults to `[]`. Add complete Kubernetes resources here when you want Helm to deploy objects such as `PodDisruptionBudget`, `NetworkPolicy`, or `PodMonitor` alongside Genesis without forking the chart. Each list item is rendered as its own YAML document.
</Tip>

<Info>
  Current Genesis container images also bundle the in-repo `genesis_sdk` package under `/app/genesis_sdk` (for `execute_python_code` local execution — not installable from PyPI) and the `llm_wiki` package under `/app/llm_wiki`, so the SDK, wiki APIs, and tools shipped with Genesis are available without extra image customization.
</Info>

<Note>
  If you use `azure-aks/deploy-aks-ga-oidc.sh`, set `ECR_REGISTRY` in `azure-aks/aks-config.env` and pin `image.tag` in your values file. The script passes `ecr.registry` and a fresh `ecr.token`, forces `image.pullPolicy=Always`, waits for the `StatefulSet` rollout to complete, and only passes `--set image.tag` when `GENESIS_IMAGE_TAG` is set for a one-off override.
</Note>

Replace:

* `<tenant-id>` with your Directory (tenant) ID
* `<application-client-id>` with your Application (client) ID

### Ingress Configuration

**For Testing (nip.io):**
Initially deploy with a placeholder host, then update with ingress IP:

```yaml theme={null}
ingress:
  enabled: true
  className: "nginx"
  hosts:
    - host: genesis-oidc.placeholder.nip.io  # Will update after getting ingress IP
      paths:
        - path: /
          pathType: Prefix
          port: 8080
```

**For Production (permanent DNS):**
After creating DNS A record, use your permanent domain:

```yaml theme={null}
ingress:
  enabled: true
  className: "nginx"
  hosts:
    - host: genesis-oidc.yourcompany.com  # Your permanent domain
      paths:
        - path: /
          pathType: Prefix
          port: 8080
  tls:
    - secretName: genesis-oidc-tls
      hosts:
        - genesis-oidc.yourcompany.com
```

## Step 4: Deploy Genesis

```bash theme={null}
# Create namespace and deploy
helm upgrade --install genesis-oidc . \
    -f Azure/my-values-oidc.yaml \
    -n genesis-oidc \
    --create-namespace

# Wait for pod to be ready
kubectl wait --for=condition=ready pod \
    -l app.kubernetes.io/instance=genesis-oidc \
    -n genesis-oidc \
    --timeout=300s
```

## Step 5: Get Ingress IP and Update Configuration

```bash theme={null}
# Get ingress IP (may take 2-5 minutes)
kubectl get ingress -n genesis-oidc -w

# Once IP is assigned, note it
INGRESS_IP=$(kubectl get ingress -n genesis-oidc genesis-oidc \
    -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Ingress IP: $INGRESS_IP"
```

### Update Helm Values

**For Testing (nip.io):**
Edit `Azure/values-aks-oidc.yaml` and update the ingress host:

```yaml theme={null}
ingress:
  hosts:
    - host: genesis-oidc.<INGRESS_IP>.nip.io  # Testing only
      paths:
        - path: /
          pathType: Prefix
          port: 8080
```

**For Production (permanent DNS):**

1. Create DNS A record: `genesis-oidc.yourcompany.com` → `<INGRESS_IP>`
2. Edit `Azure/values-aks-oidc.yaml`:

```yaml theme={null}
ingress:
  hosts:
    - host: genesis-oidc.yourcompany.com  # Your permanent domain
      paths:
        - path: /
          pathType: Prefix
          port: 8080
  tls:
    - secretName: genesis-oidc-tls
      hosts:
        - genesis-oidc.yourcompany.com
```

### Update Azure AD Redirect URIs

Keep the Genesis web UI redirect and Claude Code hosted-MCP redirect together
on the same app registration:

* The Genesis web UI redirect is the public Genesis origin.
* The Claude Code MCP redirect is `http://localhost:31318/callback`.

When enabling [Hosted MCP for Claude Code](/docs/deployments/Hosted-MCP-For-Claude-Code),
also configure a customer-facing public base URL (`GENESIS_MCP_PUBLIC_URL` or
correct ingress forwarding headers). For Anthropic Console **Connect**, set
`GENESIS_OAUTH_PROXY_ENABLED=true` on the Genesis workload so Genesis can
advertise and proxy OAuth on the same public hostname.

**For Testing (nip.io):**

```bash theme={null}
# Variables
CLIENT_ID="<your-application-client-id>"
INGRESS_IP="<your-ingress-ip>"

# Update Azure AD app registration
az rest --method PATCH \
    --uri "https://graph.microsoft.com/v1.0/applications/$(az ad app show --id $CLIENT_ID --query id -o tsv)" \
    --headers "Content-Type=application/json" \
    --body '{"spa":{"redirectUris":["http://genesis-oidc.'${INGRESS_IP}'.nip.io","https://genesis-oidc.'${INGRESS_IP}'.nip.io"]},"publicClient":{"redirectUris":["http://localhost:31318/callback"]}}'
```

**For Production (permanent domain):**

```bash theme={null}
# First create DNS A record: genesis-oidc.yourcompany.com → <INGRESS_IP>
CLIENT_ID="<your-application-client-id>"

# Update Azure AD app registration
az rest --method PATCH \
    --uri "https://graph.microsoft.com/v1.0/applications/$(az ad app show --id $CLIENT_ID --query id -o tsv)" \
    --headers "Content-Type=application/json" \
    --body '{"spa":{"redirectUris":["https://genesis-oidc.yourcompany.com"]},"publicClient":{"redirectUris":["http://localhost:31318/callback"]}}'
```

### Redeploy with Updated Values

```bash theme={null}
helm upgrade genesis-oidc . \
    -f Azure/values-aks-oidc.yaml \
    -n genesis-oidc
```

## Step 6: Verify Deployment

```bash theme={null}
# Check pod status
kubectl get pods -n genesis-oidc

# Check ingress
kubectl get ingress -n genesis-oidc

# Check logs
kubectl logs -n genesis-oidc genesis-oidc-0 --tail=50

# Test health endpoint
# Testing: curl http://genesis-oidc.<INGRESS_IP>.nip.io/api/health
# Production: curl https://genesis-oidc.yourcompany.com/api/health
```

## Step 7: Test Authentication

**Testing (nip.io):**

1. Navigate to `http://genesis-oidc.<INGRESS_IP>.nip.io/` or `https://genesis-oidc.<INGRESS_IP>.nip.io/`
2. Click **Login** — you'll be redirected to Microsoft login
3. Authenticate with your Azure AD credentials
4. After successful authentication, you'll be redirected back to Genesis

**Production (permanent domain):**

1. Navigate to `https://genesis-oidc.yourcompany.com/`
2. Click **Login** — you'll be redirected to Microsoft login
3. Authenticate with your Azure AD credentials
4. After successful authentication, you'll be redirected back to Genesis

## Step 8: Configure the LLM Provider

Genesis starts without a provider and reports **No suitable LLM found** until one is
configured. Configure it against the **running** pod, either from **Config → AI
Providers** in the UI or through the config API. Keep credentials in a request-body
file — process arguments are visible to every process on the host and land in shell
history.

```bash theme={null}
umask 077
set -o pipefail
GENESIS_URL=https://genesis-oidc.yourcompany.com
BODY=$(mktemp "${TMPDIR:-/tmp}/genesis-llm-body.XXXXXX.json")
CURL_CONFIG=$(mktemp "${TMPDIR:-/tmp}/genesis-curl.XXXXXX")
trap 'rm -f "$BODY" "$CURL_CONFIG"' EXIT
chmod 600 "$BODY" "$CURL_CONFIG"

# This deployment runs with authEnabled "true", so every call below needs a bearer
# token for a user holding access_config_panel. Without it each request returns 401.
read -rsp 'Genesis access token: ' GENESIS_TOKEN && echo
printf 'header = "Authorization: Bearer %s"\n' "$GENESIS_TOKEN" > "$CURL_CONFIG"
unset GENESIS_TOKEN

# --fail-with-body catches an HTTP error, but these routes also answer 200 with
# {"success": false}. check() turns that into a nonzero exit so the && chain stops
# instead of activating a provider whose credential never saved.
check() {
  python3 -c 'import json, sys
body = json.load(sys.stdin)
print(json.dumps(body, indent=2), flush=True)
if not body.get("success"):
    sys.exit("STOP: %s" % (body.get("message") or "call reported success: false"))'
}

cat > "$BODY" <<'JSON'
{"endpoint_name": "genesis-azureopenai-1",
 "api_key": "<azure-openai-api-key>",
 "full_endpoint_url": "https://<your-resource>.openai.azure.com/",
 "api_version": "2025-04-01-preview",
 "model_deployment_name": "<chat-deployment-name>",
 "embedding_deployment_name": "<embedding-deployment-name>",
 "test_connection": true}
JSON
curl -sS --fail-with-body --config "$CURL_CONFIG" \
  -X POST -H 'Content-Type: application/json' --data @"$BODY" \
  "$GENESIS_URL/api/config/llm-providers/azure" | check &&
# Note the two naming spaces: the card is "azure", the llm type is "azureopenai".
curl -sS --fail-with-body --config "$CURL_CONFIG" \
  -X POST -H 'Content-Type: application/json' -d '{"force": true}' \
  "$GENESIS_URL/api/config/llm-providers/azureopenai/activate" | check &&
curl -sS --fail-with-body --config "$CURL_CONFIG" \
  -X PUT -H 'Content-Type: application/json' \
  -d '{"provider": "azureopenai", "use_defaults": true, "activate_provider": true}' \
  "$GENESIS_URL/api/config/models/main/assignments" | check
```

The key is stored by Genesis through this call, not supplied as a Helm value.

If `check` stops the chain on "Configuration for … already exists", the activation and
model-assignment calls never ran — the provider is stored but not usable. Change the
**first** call from `POST` to `PUT` and rerun the **whole block**, so activation and
assignment execute after the update.
For the full endpoint reference and the other providers, see
[Configuring a provider through the config API](/docs/Setup/Setup-AI-Model#configuring-a-provider-through-the-config-api).

<Note>
  The chart still accepts `secrets.openaiApiKey`, `config.modelName` and
  `config.azureOpenaiEndpoint`; this guide's values example no longer shows them.
  Each reaches the pod as an environment variable carrying a `default_env_var`
  binding — `secrets.openaiApiKey` as `OPENAI_API_KEY`, on the OpenAI provider's key
  field. Such a binding seeds its field while nothing is stored there, and **stored
  `APP_CONFIG` values take precedence from then on**, so a value saved through the UI
  or the config API wins on every later read. They are **deprecated as an LLM setup
  path**: none of them writes `llm.active_provider`, and on an Azure OpenAI
  deployment `secrets.openaiApiKey` seeds the wrong provider — the `azureopenai`
  key field binds to `AZURE_OPENAI_API_KEY`. Configure the provider with the calls
  above, or declare it in a `genesis-config.yaml` mounted with
  `--set-file genesisConfig.content=...`.
</Note>

<Note>
  A mounted `genesis-config.yaml` is a third configuration surface. An unchanged file
  fills only the paths that are missing, so later UI edits survive a restart; when the
  profile path or the file's hash changes, Genesis clears and rewrites every top-level
  section the file contains. Treat the file as the canonical source wherever it is used.
</Note>

## Helm-driven RBAC

For production OIDC deployments (for example Azure Entra ID on AKS), you can
declare RBAC outside the Genesis UI using Helm values. Genesis applies both
features on startup and on every login.

### Group-to-role mapping (`groupRoleMapping`)

Maps external identity-provider group identifiers (for example Entra security
group GUIDs from the JWT `groups` claim) to Genesis role names. On each login,
Genesis syncs the user's roles from group membership.

Provide a YAML file via `--set-file`:

```bash theme={null}
helm upgrade --install genesis-oidc . \
  -f Azure/my-values-oidc.yaml \
  --set-file groupRoleMapping.content=group-role-mapping.yaml \
  -n genesis-oidc
```

Example `group-role-mapping.yaml`:

```yaml theme={null}
"2d8b2b8d-a652-4eed-9939-a08238e58af9": "admin"
"5dba4ec0-2ddd-4203-abc9-6fb730cbe0f5": "user"
```

**Sync behavior:**

* Roles are added when the user gains membership in a mapped group.
* Roles are removed when the user loses membership — but only for roles that
  appear as values in the mapping. Manually assigned roles outside the mapping
  are never removed.
* The sync runs on every login, not only first login.

**Entra prerequisite:** Under **Token configuration**, add a **groups** claim
with **Security groups** and emit group identifiers as GUIDs.

Genesis mounts the file at `/app/.genesis/config/group-role-mapping.yaml` and
sets `GENESIS_GROUP_ROLE_MAPPING_FILE`. Without Helm, you can instead set
`GENESIS_GROUP_ROLE_MAPPING` to a JSON object with the same key/value shape.

### RBAC seed policies (`rbacSeedPolicy`)

Pre-populate custom roles, permissions, and role hierarchy at startup using
standard Casbin CSV format. Provide via `--set-file`:

```bash theme={null}
helm upgrade --install genesis-oidc . \
  -f Azure/my-values-oidc.yaml \
  --set-file rbacSeedPolicy.content=rbac-policies.csv \
  -n genesis-oidc
```

Example `rbac-policies.csv`:

```csv theme={null}
# Role hierarchy — custom_role inherits built-in user permissions
g, custom_role, user
# Permissions
p, custom_role, apps, view
p, custom_role, agent:Adam, access
```

Genesis mounts the file at `/app/.genesis/config/rbac-policies.csv` and sets
`GENESIS_RBAC_SEED_FILE`. Policies are idempotent — existing rules are not
duplicated. Lines starting with `#` and blank lines are skipped.

You can also set default roles for brand-new users (before the admin UI
override) in `genesis-config.yaml`:

```yaml theme={null}
rbac:
  default_roles:
    - user
```

See [RBAC in Genesis: A Practical Guide](/docs/Security-Controls/RBAC-Practical-Guide#oidc-group-to-role-mapping-helm-driven) and [Advanced Settings → RBAC defaults](/docs/Setup/Advanced-Settings#rbac-defaults).

## Configuration Reference

### Environment Variables

| Variable | Required | Description |
| - | - | - |
| `AUTH_ENABLED` | Yes | Set to `true` to enable authentication |
| `AUTH_PROVIDER` | Yes | Set to `oidc` for native OIDC |
| `AUTH_ISSUER` | Yes | OIDC issuer URL (e.g., `https://login.microsoftonline.com/{tenant}/v2.0`) |
| `AUTH0_CLIENT_ID` | Yes | Application (Client) ID |
| `AUTH0_DOMAIN` | No | Override for `auth0_domain` returned by `GET /api/config/frontend`. When set, takes precedence over the domain derived from `AUTH_ISSUER`. Use for Entra when the issuer host alone is not the correct OAuth path prefix (for example `login.microsoftonline.com/{tenant}/oauth2/v2.0`). |
| `AUTH_AUDIENCE` | No | JWT audience claim (typically same as Client ID) |
| `AUTH_JWKS_URL` | No | JWKS URL (auto-discovered if not set) |
| `AUTH_ALLOW_QUERY_TOKEN` | No | Allow token in query params for WebSocket (default: `false`) |
| `GENESIS_GROUP_ROLE_MAPPING_FILE` | No | Path to a YAML group-to-role map (set automatically when `groupRoleMapping.content` is provided) |
| `GENESIS_RBAC_SEED_FILE` | No | Path to a Casbin CSV seed file (set automatically when `rbacSeedPolicy.content` is provided) |

### Azure AD URLs

For tenant ID `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`:

* **Issuer URL**: `https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/v2.0`
* **JWKS URL**: `https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/discovery/v2.0/keys`

## Reference: Example values file

The following is a production-ready example `values-aks-oidc.yaml` for Genesis on Azure AKS with native OIDC. Replace placeholders (e.g. `<tenant-id>`, `<application-client-id>`, `<your-resource-name>`, `genesis-oidc.yourcompany.com`) with your own values.

```yaml theme={null}
# Genesis Helm Chart - Azure AKS Values with Native OIDC Authentication
# Production-ready configuration for Azure Kubernetes Service
#
# This configuration uses Native OIDC authentication (no OAuth2 Proxy needed).
# Genesis handles authentication directly using Authorization Code Flow + PKCE.
#
# Compatible with: Azure AD (Entra ID), Okta, Keycloak, and any OIDC provider.
#
# Required configuration:
#   1. Configure OIDC settings in config.extraEnv (see below)
#
# The LLM provider is NOT configured in this file. It is configured after the pod
# is running, from Config -> AI Providers or through the config API — see
# "Step 8: Configure the LLM Provider".
#
# OIDC Environment Variables (set in config.extraEnv):
#   AUTH_ISSUER: "https://login.microsoftonline.com/<tenant-id>/v2.0"
#   AUTH0_CLIENT_ID: "<application-client-id>"  # Used for backward compatibility
#   AUTH_AUDIENCE: "<application-client-id>"
#   AUTH_JWKS_URL: "https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys"  # Optional
#   AUTH_ALLOW_QUERY_TOKEN: "true"  # For WebSocket support

# Namespace configuration
namespace: genesis-oidc

# Container image configuration
image:
  repository: ghcr.io/genesis-computing-ai/genesis
  tag: v1.18.2
  pullPolicy: IfNotPresent

# Genesis AKS bundle uses ECR by default; registry/token are injected by the helper script
ecr:
  enabled: true
  registry: ""

# Leave empty when the helper script injects the ECR pull secret
imagePullSecrets: []

# Secrets configuration (sensitive values only)
# IMPORTANT: Use Azure Key Vault or --set flags for production
secrets:
  # Master encryption key for Genesis secrets management (REQUIRED on first
  # install unless existingSecretGenesisMasterKey or
  # config.extraEnv.GENESIS_MASTER_KEY_REF is set)
  # Generate with: python -c "import secrets; print(secrets.token_hex(32))"
  genesisMasterKey: ""
  # Or reference an existing Kubernetes Secret
  existingSecretGenesisMasterKey:
    secretName: ""
    secretKey: "GENESIS_MASTER_KEY"
  # Or resolve from GCP Secret Manager at startup (Workload Identity):
  # config.extraEnv.GENESIS_MASTER_KEY_REF: "gcp_prod/genesis-keys#master_key"
  
  # No LLM provider secret appears here. The chart still accepts
  # secrets.openaiApiKey / secrets.databricksApiKey, but they are deprecated
  # bootstrap seeds — see the note under "Step 8: Configure the LLM Provider".
  
  # Databricks Database Connection PAT (SQL warehouse, not the LLM provider)
  databricksDbPat: ""
  existingSecretDatabricksDbPat:
    secretName: ""
    secretKey: "DATABRICKS_DB_PAT"

# Non-sensitive configuration
config:
  # Database path (inside the persistent volume)
  sqliteDbPath: "/app/.genesis/db/genesis.db"
  
  # Log level: DEBUG, INFO, WARNING, ERROR
  logLevel: "INFO"
  
  # Log format: json, text
  logsFormat: "json"  # Use json for Azure Log Analytics integration
  
  # --------------------------------------------------------------------------
  # Authentication Configuration (Native OIDC)
  # --------------------------------------------------------------------------
  # Genesis handles authentication directly - no OAuth2 Proxy required
  authEnabled: "true"
  authProvider: "oidc"  # Native OIDC support (Authorization Code + PKCE)
  
  # --------------------------------------------------------------------------
  # LLM model configuration is not set here
  # --------------------------------------------------------------------------
  # The chart still accepts config.modelName / fastModelName / harvesterModel /
  # embeddingModel / maxTokens / azureOpenaiEndpoint as deprecated bootstrap
  # seeds. Model assignments and the endpoint are saved with the provider in
  # Step 8, so they are not shown in this example.
  
  # --------------------------------------------------------------------------
  # Databricks-specific configuration
  # --------------------------------------------------------------------------
  databricksBaseUrl: ""
  databricksHost: ""
  databricksDb:
    enabled: false
    connectionId: "databricks"
    serverHostname: ""
    httpPath: ""
    description: "Databricks SQL Warehouse"
    # Authentication type: "pat" (default) or "user_oauth" (per-user OAuth)
    authType: "pat"
  
  # --------------------------------------------------------------------------
  # OIDC Authentication Environment Variables
  # --------------------------------------------------------------------------
  # These variables configure the native OIDC authentication
  # Replace placeholders with your Azure AD (or other IdP) values
  extraEnv:
    # OIDC Issuer URL (Azure AD v2.0 endpoint)
    AUTH_ISSUER: "https://login.microsoftonline.com/<tenant-id>/v2.0"
    
    # Application (Client) ID from Azure AD App Registration
    AUTH0_CLIENT_ID: "<application-client-id>"
    
    # JWT Audience - typically the same as Client ID
    AUTH_AUDIENCE: "<application-client-id>"
    
    # JWKS URL for JWT verification
    AUTH_JWKS_URL: "https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys"
    
    # Allow token in query parameter (required for WebSocket support)
    AUTH_ALLOW_QUERY_TOKEN: "true"
    
    # Optional: see Configuration Reference — AUTH0_DOMAIN
    AUTH0_DOMAIN: "login.microsoftonline.com/<tenant-id>/oauth2/v2.0"

# Helm-driven RBAC (optional)
groupRoleMapping:
  content: ""
rbacSeedPolicy:
  content: ""

# Resource requests (production sizing)
resources:
  requests:
    cpu: "2000m"
    memory: "6Gi"
  limits:
    cpu: "4000m"
    memory: "8Gi"

# Persistent storage configuration
# AKS uses Azure Disk (default storage class)
persistence:
  storageClassName: "managed-csi"  # Azure Disk CSI driver (default in AKS)
  size: 50Gi
  accessMode: ReadWriteOnce

# Service configuration
service:
  type: ClusterIP
  ports:
    fastapi: 8080
    flask: 8082
    streamlit: 8501

# --------------------------------------------------------------------------
# Ingress Configuration
# --------------------------------------------------------------------------
# With native OIDC, Genesis handles auth directly - ingress goes to Genesis
# (No OAuth2 Proxy needed)
ingress:
  enabled: true
  className: "nginx"
  annotations:
    # WebSocket support
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
  
  # Host configuration
  hosts:
    # Option 1: For testing/development - Use nip.io with ingress IP
    # Step 1: Deploy with placeholder
    # Step 2: Get ingress IP: kubectl get ingress -n genesis-oidc
    # Step 3: Update host to: genesis-oidc.<INGRESS_IP>.nip.io
    # Step 4: helm upgrade genesis-oidc . -f values-aks-oidc.yaml -n genesis-oidc
    - host: genesis-oidc.yourcompany.com  # Your permanent domain
      paths:
        - path: /
          pathType: Prefix
          port: 8080
    
    # Option 2: For production - Use your real domain
    # - host: genesis.yourcompany.com
    #   paths:
    #     - path: /
    #       pathType: Prefix
    #       port: 8080
  
  # TLS configuration
  tls: []
  # Example with cert-manager:
  # tls:
  #   - secretName: genesis-oidc-tls
  #     hosts:
  #       - genesis.yourdomain.com

# Health probes
startupProbe:
  httpGet:
    path: /api/health
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 60

livenessProbe:
  httpGet:
    path: /api/health/live
    port: 8080
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /api/health
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

# Pod annotations
podAnnotations: {}

# Node selector (optional)
nodeSelector: {}

# Tolerations (optional)
tolerations: []

# Affinity rules (optional)
affinity: {}

# Extra volumes (e.g., Azure Key Vault CSI driver)
extraVolumes: []

# Extra volume mounts
extraVolumeMounts: []
```

<Tip>
  When `config.databricksDb.authType` is `user_oauth`, Genesis bootstraps a
  per-user OAuth Databricks connection at startup using
  `DATABRICKS_DB_SERVER_HOSTNAME` and `DATABRICKS_DB_HTTP_PATH` only — no
  `DATABRICKS_DB_PAT` secret is required. Each user authorizes their own grant
  at runtime. When `authType` is `pat` (default), provide
  `secrets.databricksDbPat` or `existingSecretDatabricksDbPat`.
</Tip>

## Troubleshooting

### "AADSTS500113: No reply address is registered"

**Cause**: No Redirect URI configured in Azure AD.

**Fix**:

1. Azure Portal → App registrations → Your app → **Authentication**
2. Add platform: **Single-page application**
3. Add redirect URI: `http://genesis-oidc.<IP>.nip.io` (no trailing slash, no path)

### "AADSTS50011: Reply URL does not match"

**Cause**: Redirect URI mismatch.

**Fix**:

* For Genesis web login, register exactly: `http://genesis-oidc.<IP>.nip.io` or `https://genesis-oidc.yourcompany.com` (no trailing `/`)
* For Claude Code hosted MCP login, register exactly: `http://localhost:31318/callback` as a **Mobile and desktop applications** redirect URI
* Check browser DevTools → Network → look at `redirect_uri` in the `/authorize` request

### Login works but no email in Genesis

**Cause**: Azure AD doesn't include email in tokens by default.

**Fix**:

1. Azure Portal → App registrations → Your app → **Token configuration**
2. Click "Add optional claim"
3. Select **ID** token type
4. Check: `email`, `upn`, `preferred_username`
5. Save and re-login

### Token Refresh Failure / Infinite Login Loop

**Cause**: Missing `offline_access` scope.

**Fix**:

* The frontend automatically requests `openid profile email offline_access`
* Verify Azure AD allows refresh tokens for SPAs (enabled by default)

### CORS Errors

**Cause**: Usually a misconfiguration in Azure AD.

**Fix**:

* Azure AD enables CORS by default for SPA apps
* Verify the platform type is "Single-page application"

### WebSocket Connection Fails

**Cause**: Token not sent or invalid.

**Fix**:

* Set `AUTH_ALLOW_QUERY_TOKEN: "true"` in config
* Check that token hasn't expired
* Check browser DevTools → Network → WS → Headers

### Pod Not Starting

```bash theme={null}
# Check pod status
kubectl describe pod -n genesis-oidc genesis-oidc-0

# Check events
kubectl get events -n genesis-oidc --sort-by='.lastTimestamp'

# Check logs
kubectl logs -n genesis-oidc genesis-oidc-0 --previous
```

## Comparison: OIDC vs OAuth2 Proxy

| Aspect | Native OIDC | OAuth2 Proxy |
| - | - | - |
| Components | Genesis only | Genesis + OAuth2 Proxy |
| Configuration | Helm values only | Helm values + oauth2-proxy-values.yaml |
| Cookie handling | Standard JWT | Large session cookies (needs buffer tuning) |
| 502 errors | Unlikely | Common (buffer size issues) |
| Maintenance | Lower | Higher (two components) |
| Token refresh | Browser-native | Proxy-managed |

If you choose OAuth2 Proxy instead of native OIDC, use the Helm bundle's `azure-aks/oauth2-proxy-values.yaml` example (see also `azure-aks/AZURE_AD_AKS_SETUP.md`). Required oauth2-proxy settings for dashboard and Kubernetes health probes:

```yaml theme={null}
cookie_secure = true
cookie_samesite = "lax"
skip_auth_routes = ["GET=^/api/health$", "GET=^/api/health/startup$", "GET=^/api/health/live$", "GET=^/api/healthcheck$"]
cookie_csrf_per_request = true
cookie_csrf_expire = "5m"
scope = "openid email profile offline_access"
# cookie_refresh = "55m"
```

`cookie_secure = true` applies when HTTPS terminates at the ingress or external load balancer. In this guide's reverse-proxy mode (`upstreams = [...]`), `skip_auth_routes` exempts only the GET probe endpoints above; other `/api/health/*` routes stay behind authentication. Under the nginx `auth_request` / `nginx.ingress.kubernetes.io/auth-url` pattern, oauth2-proxy sees only `/oauth2/auth`, so `skip_auth_routes` does not match application paths. Use a second Ingress on the same host without the auth annotations to exempt the health endpoints there.

Keep `offline_access` in `scope` when using `cookie_refresh`: Entra ID returns a refresh token only for requests that include this scope. Without it, oauth2-proxy cannot renew the session when the Entra access token expires (about 1 hour by default), and the user is sent through sign-in again. `cookie_refresh` is not enabled in this example; if you enable it, set it below the access-token lifetime, such as `55m`. Adding `offline_access` to an existing app registration may prompt users for consent on their next sign-in, depending on tenant consent policy and existing grants. Grant admin consent once from Azure AD → App registrations → your app → API permissions → **Grant admin consent** to avoid that prompt.

Keep `cookie_csrf_per_request = true` to give concurrent sign-in flows separate CSRF cookies and prevent invalid-CSRF `403` errors. The trade-off is that abandoned flows leave cookies behind, so keep `cookie_csrf_expire = "5m"`; on oauth2-proxy 7.11 and later, set `cookie_csrf_per_request_limit` if needed to cap accumulation and avoid request headers growing until nginx returns `431` or `502`.

## Cleanup

To remove the deployment:

```bash theme={null}
# Delete Genesis
helm uninstall genesis-oidc -n genesis-oidc

# Delete namespace (optional)
kubectl delete namespace genesis-oidc
```

<Info>
  PVCs are retained by default. To delete: `kubectl delete pvc -n genesis-oidc --all`

  For backup and snapshot strategies (CSI snapshots, Azure Backup for AKS, Velero, logical exports), see [Backup and Snapshot Options](/docs/deployments/Azure/Backup-And-Snapshot-Options).
</Info>

## Production DNS Setup

**Important:** For production deployments, you must configure permanent DNS names:

1. **Get your ingress IP:**
   ```bash theme={null}
   kubectl get ingress -n genesis-oidc genesis-oidc \
       -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
   ```

2. **Create DNS A record:**
   * In your DNS provider (Azure DNS, Route 53, etc.)
   * Create A record: `genesis-oidc.yourcompany.com` → `<INGRESS_IP>`

3. **Update Helm values:**
   * Edit `Azure/values-aks-oidc.yaml` to use your permanent domain
   * Update ingress hosts to use your domain
   * Add TLS configuration

4. **Update Azure AD redirect URIs:**
   * Remove `nip.io` redirect URIs
   * Add your permanent domain redirect URI: `https://genesis-oidc.yourcompany.com`

5. **Redeploy:**
   ```bash theme={null}
   helm upgrade genesis-oidc . -f Azure/values-aks-oidc.yaml -n genesis-oidc
   ```

## Next Steps

* See your Genesis deployment bundle for **Azure Key Vault setup** (storing secrets securely), **TLS configuration** (HTTPS), and **production checklist** (hardening).
* For general AKS deployment without OIDC, see [Deploying Genesis - Kubernetes on Azure](/docs/deployments/Azure/Genesis-On-Azure).
