# OIDC (Dex)

Sign in to the hub with GitHub, Google, LDAP, or any OIDC provider via Dex.

Source: https://railgrid.ai/docs/self-hosting/hub/oidc/


For teams or anywhere you want real per-user identities, use OIDC. The hub itself does not care which OIDC provider you use, but the easiest path is [Dex](https://dexidp.io/): it sits in front of GitHub / Google / LDAP / SAML / OIDC backends and presents one OIDC endpoint to the hub.

```
Browser ──▶ Hub ──▶ Dex ──▶ Identity backend (GitHub, Google, LDAP, …)
   │                                              │
   └────────── OIDC redirect ────────────────────┘
```

With [external kcp](https://railgrid.ai/docs/self-hosting/hub/external-kcp/), kcp's front proxy and shards verify the same tokens, so they are configured with the same issuer and client.

## Prerequisites

- A publicly reachable HTTPS URL for Dex, served through your [ingress](https://railgrid.ai/docs/self-hosting/hub/ingress/). The hub reads Dex's discovery document at start and restarts until it can.
- An identity backend: a GitHub OAuth App, Google credentials, an LDAP server, and so on.
- The hub's public URL, for the client's redirect URI.

## 1. Deploy Dex

```bash
helm repo add dex https://charts.dexidp.io
helm repo update
kubectl create namespace dex
```

Dex needs somewhere to keep signing keys, auth requests, and refresh tokens. Two options:

- **Kubernetes storage.** Dex stores its state as custom resources in its namespace. No volume, no database, and it works with more than one replica; losing it signs everyone out and nothing more. This is what Railgrid runs.
- **SQLite on a PVC.** Simplest for a single replica with no CRDs.

Put the connector secret in a Secret and let Dex expand it from the environment rather than writing it into the values file:

```bash
kubectl -n dex create secret generic dex-secrets \
  --from-literal=GITHUB_CLIENT_SECRET="<github-client-secret>"
```

Dex values (`dex-values.yaml`), Kubernetes storage:

```yaml
replicaCount: 2

config:
  # Must equal idp.issuerURL in the hub values, character for character.
  issuer: https://auth.example.com

  storage:
    type: kubernetes
    config:
      inCluster: true

  # Plain HTTP in the cluster; TLS ends at your ingress.
  web:
    http: 0.0.0.0:5556

  staticClients:
    # Public PKCE client used by the console, `railgrid login`, and, as the
    # token audience, kcp. Must match idp.clientID.
    - id: railgrid
      name: Railgrid
      public: true          # no client secret
      redirectURIs:
        - https://console.example.com/auth/callback
        - http://localhost:8000

  connectors:
    - type: github
      id: github
      name: GitHub
      config:
        clientID: <github-client-id>
        clientSecret: $GITHUB_CLIENT_SECRET     # expanded from dex-secrets
        redirectURI: https://auth.example.com/callback
        # Optional: only members of this organization may sign in.
        org: your-org

envFrom:
  - secretRef:
      name: dex-secrets

service:
  type: ClusterIP

# Route auth.example.com yourself (Gateway API, cfgate) or enable the
# chart's Ingress for an ingress controller:
ingress:
  enabled: false
```

For SQLite instead, create a 1 GiB `PersistentVolumeClaim` named `dex-data`, set `storage.type: sqlite3` with `config.file: /var/dex/dex.db`, mount the claim at `/var/dex` through `volumes`/`volumeMounts`, and keep `replicaCount: 1`.

Install Dex:

```bash
helm upgrade --install dex dex/dex --version 0.24.1 \
  --namespace dex -f dex-values.yaml
```

Railgrid runs chart `0.24.1`. Verify the issuer through its public URL, because that is what the hub and kcp will use:

```bash
curl -s https://auth.example.com/.well-known/openid-configuration | jq -r .issuer
# https://auth.example.com
```

Dex answers 404 on `/`. A load balancer that health-checks `/` will mark it down; check `/healthz` instead (see [Gateway API](https://railgrid.ai/docs/self-hosting/hub/gateway-api/#route-dex)).

## 2. Configure the hub for OIDC

Update your hub values: remove `staticAuthTokens` (or keep them; both methods can coexist) and add the `idp` section:

```yaml
hub:
  hubExternalURL: "https://console.example.com"

idp:
  issuerURL: "https://auth.example.com"
  clientID: "railgrid"
```

There is **no client secret**: the hub, CLI, and console are PKCE public clients. That is why the Dex client above is marked `public: true`.

If your IdP serves a certificate signed by a private CA, put the PEM bundle in a Secret and reference it. The hub then verifies the issuer properly instead of you disabling TLS checks:

```yaml
idp:
  issuerURL: "https://auth.example.com"
  clientID: "railgrid"
  caSecretName: "idp-ca"      # Secret with the PEM bundle
  caSecretKey: "ca.crt"       # key inside the Secret
```

Roll out, with `HUB_CHART_VERSION` resolved as in [Install the hub](https://railgrid.ai/docs/self-hosting/hub/helm/#prerequisites):

```bash
helm upgrade --install railgrid-hub oci://ghcr.io/railgrid/charts/railgrid-hub \
  --version "$HUB_CHART_VERSION" -f hub-values.yaml --namespace railgrid-system
```

## 3. Configure kcp (external kcp only)

Every kcp component that authenticates users needs the same issuer and client, with the identity prefixes the hub hard-codes. On the `FrontProxy`, the `RootShard`, and every `Shard`:

```yaml
auth:
  oidc:
    issuerURL: https://auth.example.com
    clientID: railgrid
    groupsClaim: groups
    usernameClaim: email
    usernamePrefix: "railgrid:"
    groupsPrefix: "railgrid:"
```

Any other prefix denies every request, because workspace RBAC is bound to `railgrid:<email>`. If the issuer's certificate chains to a root the kcp image does not ship (a cloud-managed edge certificate, for example), set `spec.caBundleSecretRef` on the `FrontProxy` to a Secret holding that chain.

## 4. Log in

```bash
kubectl railgrid login --hub-url https://console.example.com
```

The browser opens the hub's `/auth/authorize` endpoint, which redirects to Dex, which redirects to your identity backend. After you authorize, the CLI receives your kubeconfig over a one-time localhost callback. The kubeconfig uses an exec credential plugin that refreshes tokens automatically; see [Login & Authentication](https://railgrid.ai/docs/reference/cli/login/).

## Identity connectors

Dex supports many backends. The most common configurations:

### GitHub

1. **GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.**
2. **Authorization callback URL**: `https://auth.example.com/callback`. An OAuth App has exactly one callback URL, so the Code provider's "Connect with GitHub" feature needs its own app; see [Self-host Code](https://railgrid.ai/docs/self-hosting/providers/code/).
3. Add to Dex `connectors`:

```yaml
- type: github
  id: github
  name: GitHub
  config:
    clientID: <github-client-id>
    clientSecret: $GITHUB_CLIENT_SECRET
    redirectURI: https://auth.example.com/callback
    # Optional: restrict to organization members
    org: your-org
```

### Google

1. Create OAuth credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials).
2. Add to Dex `connectors`:

```yaml
- type: google
  id: google
  name: Google
  config:
    clientID: <google-client-id>
    clientSecret: $GOOGLE_CLIENT_SECRET
    redirectURI: https://auth.example.com/callback
    # Optional: restrict to a Workspace domain
    hostedDomains:
      - example.com
```

### LDAP

```yaml
- type: ldap
  id: ldap
  name: LDAP
  config:
    host: ldap.example.com:636
    insecureNoSSL: false
    bindDN: cn=admin,dc=example,dc=com
    bindPW: $LDAP_BIND_PASSWORD
    userSearch:
      baseDN: ou=users,dc=example,dc=com
      filter: "(objectClass=person)"
      username: uid
      idAttr: uid
      emailAttr: mail
      nameAttr: cn
```

## Keep accounts across connector and issuer changes

A Railgrid account is found at sign-in by `issuer + subject`. Dex encodes the connector into the subject, so a person who signed in with GitHub and later signs in with Google is a different subject; moving to another issuer changes both. Without preparation, every returning user gets a fresh, empty account.

`idp.linkVerifiedEmailIssuers` lists issuers whose not-yet-known identities may be linked onto the one existing account with the same verified email:

```yaml
idp:
  linkVerifiedEmailIssuers:
    - https://auth.example.com
```

Listing an issuer trusts it to have verified email ownership for every connector it fronts; the GitHub and Google connectors do. Linking only acts on identities no account holds yet, so it can stay on. kcp is unaffected either way: it identifies users as `railgrid:<email>`.

To add a connector: add it to Dex, set the hub value, and people who sign in through the new connector land on their existing account. To move to another issuer: register the public client there (redirects `<hub>/auth/callback` and `http://localhost:8000`, `email` and `email_verified` in the ID token), set `idp.issuerURL` and add the new issuer to the list, update the kcp OIDC blocks, and have everyone sign in again.

## Troubleshooting

**`invalid issuer` from the hub.** `config.issuer` in Dex must match `idp.issuerURL` in the hub byte for byte. Same scheme, no trailing slash.

**Callback URL mismatch.** `redirectURIs` in Dex's `staticClients` must include exactly `<hubExternalURL>/auth/callback`. Mismatched scheme or path is the usual culprit.

**Hub restarts on start.** It cannot fetch the issuer's discovery document. Check that `https://auth.example.com/.well-known/openid-configuration` answers from inside the cluster, including DNS and TLS.

**TLS errors on the issuer.** If Dex's certificate is signed by a private CA, set `idp.caSecretName` / `idp.caSecretKey` (see above), and `spec.caBundleSecretRef` on the kcp front proxy. As a last resort for throwaway dev setups, `hub.devMode: true` skips issuer TLS verification. Never in production.

**`unauthorized_client` or secret prompts.** The Dex client must be `public: true` with no `secret`. Railgrid does PKCE; a confidential client configuration will fail.

**Signed in to the console, but every kcp request is forbidden.** The kcp OIDC block differs from the hub's: wrong issuer or client, or a missing `railgrid:` prefix.

**Dex logs**:

```bash
kubectl -n dex logs -l app.kubernetes.io/name=dex
```

**Discovery sanity check**:

```bash
curl -s https://auth.example.com/.well-known/openid-configuration | jq .
```
