# Install the hub

Choose embedded or external kcp, configure a production hub, and verify the console.

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


## Choose a deployment

| Mode | Hub workload | Constraint |
|---|---|---|
| Embedded kcp | StatefulSet | One replica; embedded etcd uses its PVC |
| External kcp | Deployment | Hub replicas can scale; operate kcp and its storage separately ([guide](https://railgrid.ai/docs/self-hosting/hub/external-kcp/)) |

A hub installation does not install every provider. Install the providers your users need after verifying the hub.

## Prerequisites

Use a Kubernetes cluster you administer, Helm, kubectl, and a reviewed Railgrid chart version. For public access, configure DNS, TLS, and [ingress](https://railgrid.ai/docs/self-hosting/hub/ingress/). Use development credentials only in an isolated test installation.

The chart is published as `oci://ghcr.io/railgrid/charts/railgrid-hub`. Its version is the Railgrid release without the `v` prefix (release `vX.Y.Z` is chart `X.Y.Z`), and its `appVersion` names the matching hub image. Resolve the version once and reuse it in every command below. Helm reports the newest published chart when no version is given:

```bash
HUB_CHART_VERSION=$(helm show chart oci://ghcr.io/railgrid/charts/railgrid-hub \
  | awk '/^version:/ {print $2}')
echo "$HUB_CHART_VERSION"
```

To install a release you have reviewed rather than the newest one, set `HUB_CHART_VERSION` to that release's chart version instead. The [releases page](https://github.com/railgrid/railgrid/releases) lists them.

## Local hub

For a laptop or a throwaway evaluation, do not install the chart by hand. `railgrid dev init` creates one kind cluster running the hub on embedded kcp, the default providers, and an agent that joins that same cluster as an edge, then signs you in with a static development token at `https://console.127.0.0.1.sslip.io:9443`:

```bash
railgrid dev init
```

`railgrid dev delete` removes it again. [Run Railgrid locally](https://railgrid.ai/docs/get-started/local/) covers prerequisites, options, and troubleshooting, and the [`railgrid dev init` reference](https://railgrid.ai/docs/reference/cli/commands/dev-init/) lists every flag, including `--chart-path` for a hub chart you are changing. That environment is not a production installation; the rest of this page configures a hub other people use.

## Configure a production hub

For a shared cluster, prepare DNS, [ingress and TLS](https://railgrid.ai/docs/self-hosting/hub/ingress/), and [OIDC](https://railgrid.ai/docs/self-hosting/hub/oidc/) first. The values below are what a production hub against [external kcp](https://railgrid.ai/docs/self-hosting/hub/external-kcp/) looks like. Substitute your hostnames, identity provider, administrators, and proxy ranges. For embedded kcp, drop the `kcp` block and keep `replicaCount: 1`.

Save this as `hub-values.yaml`:

```yaml
replicaCount: 2                 # more than 1 requires external kcp

kcp:
  external:
    enabled: true
    existingSecret: railgrid-kcp   # key admin.kubeconfig; see External kcp

hub:
  hubExternalURL: https://console.example.com
  # In-cluster address baked into minted provider kubeconfigs, so
  # provider -> hub traffic never leaves the cluster. The serving
  # certificate below must cover this name.
  internalURL: https://railgrid-hub.railgrid-system.svc.cluster.local:9443
  adminUsers:
    - admin@example.com
  # Reverse proxies in front of the hub (ingress pods, load balancer).
  # Required behind any proxy; see Ingress.
  trustedProxyCIDRs:
    - 10.244.0.0/16
  # Hosts the console may embed in an iframe: App Studio previews
  # under the infrastructure provider's apps domain.
  portalFrameSources:
    - https://*.apps.example.com
  security:
    providerDelegatedTokens: platform
  tls:
    # A certificate you issue (below). Setting this turns the chart's
    # selfSigned and certManager modes off.
    existingSecret: railgrid-hub-serving-tls
  # CA bundle embedded in every minted provider kubeconfig.
  providerCASecretName: railgrid-provider-ca

idp:
  issuerURL: https://auth.example.com
  clientID: railgrid
  # Keep accounts when you add a Dex connector or change issuer.
  linkVerifiedEmailIssuers:
    - https://auth.example.com
```

Everything the example leaves out keeps the chart default: development mode is off, the chart creates no Ingress or HTTPRoute, and the hub image is the chart's `appVersion`. Route traffic with your own Ingress or HTTPRoute (see [Ingress](https://railgrid.ai/docs/self-hosting/hub/ingress/)), or enable `ingress` or `httpRoute` when the chart's objects fit your controller.

Set `adminUsers` to identities as resolved by your authentication setup: the email for OIDC users. An empty list disables the admin surface, including provider onboarding.

Before installing, create the Secrets the values reference: the kcp kubeconfig (external kcp only), the serving certificate, and the provider CA bundle. The next section covers the certificates. Then render and install:

```bash
helm template railgrid-hub oci://ghcr.io/railgrid/charts/railgrid-hub \
  --version "$HUB_CHART_VERSION" --namespace railgrid-system --values hub-values.yaml
helm upgrade --install railgrid-hub oci://ghcr.io/railgrid/charts/railgrid-hub \
  --version "$HUB_CHART_VERSION" --kube-context HOSTING_CONTEXT \
  --namespace railgrid-system --create-namespace \
  --values hub-values.yaml --wait --timeout 15m
```

Name the release `railgrid-hub`. The chart's full name then collapses to `railgrid-hub`, which is the Service name the provider charts default to in `hub.url`.

Replace `HOSTING_CONTEXT` with the hosting cluster's kubeconfig context. The hub reads the OIDC discovery document at start and restarts until the issuer is reachable, so install Dex first. A successful Helm rollout verifies workload readiness; complete the authentication and workspace checks below before giving users access.

### Serving certificate and provider trust

Providers verify the hub's certificate. A minted provider kubeconfig carries no `insecure-skip-tls-verify` unless the hub runs in development mode; it embeds the CA bundle from `hub.providerCASecretName` (Secret key `ca.crt`) and trusts nothing else. Two consequences:

- The hub's serving certificate must be issued by a CA you control and keep stable. The chart's `tls.selfSigned` mode generates a fresh CA on every upgrade, which invalidates every minted kubeconfig on the next release. Issue the certificate yourself.
- With external kcp, providers also follow APIExport endpoints to the shards and verify the shards' certificates with the same bundle. Append kcp's `root-server-ca` certificate (Secret `root-server-ca` in the kcp namespace) to the bundle, or providers start but report `x509: certificate signed by unknown authority` against the shard hostnames and stay unready.

With cert-manager installed, this creates a ten-year CA, an Issuer from it, and a one-year serving certificate. The hub reads its certificate once at start; the long duration and early renewal mean any hub rollout inside the renewal window picks the new certificate up. Browsers never see this certificate when TLS terminates at your edge; providers do.

```yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: selfsigned
  namespace: railgrid-system
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: railgrid-hub-ca
  namespace: railgrid-system
spec:
  isCA: true
  commonName: railgrid-hub-ca
  secretName: railgrid-hub-ca
  duration: 87600h    # 10y; providers embed it, so rotating it means re-minting them
  renewBefore: 8760h
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Never
  issuerRef:
    name: selfsigned
    kind: Issuer
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: railgrid-hub-ca
  namespace: railgrid-system
spec:
  ca:
    secretName: railgrid-hub-ca
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: railgrid-hub-serving
  namespace: railgrid-system
spec:
  secretName: railgrid-hub-serving-tls
  duration: 8760h     # 1y
  renewBefore: 2160h  # 90d
  dnsNames:
    - console.example.com
    - railgrid-hub
    - railgrid-hub.railgrid-system.svc
    - railgrid-hub.railgrid-system.svc.cluster.local
    - localhost
  ipAddresses:
    - 127.0.0.1
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always
  issuerRef:
    name: railgrid-hub-ca
    kind: Issuer
```

Build the provider CA bundle from the CA certificate, plus kcp's server CA when kcp is external:

```bash
kubectl --context HOSTING_CONTEXT -n railgrid-system get secret railgrid-hub-ca \
  -o jsonpath='{.data.ca\.crt}' | base64 -d > provider-ca.pem
# External kcp only:
kubectl --context HOSTING_CONTEXT -n kcp-system get secret root-server-ca \
  -o jsonpath='{.data.tls\.crt}' | base64 -d >> provider-ca.pem

kubectl --context HOSTING_CONTEXT -n railgrid-system create secret generic railgrid-provider-ca \
  --from-file=ca.crt=provider-ca.pem
```

The bundle contains only public certificates and can live in version control. If you change it later, restart the hub and re-copy every provider's kubeconfig Secret: the hub rewrites the minted Secrets on reconcile, but the copies in provider namespaces do not update themselves. See [Provider hosting](https://railgrid.ai/docs/self-hosting/providers/#refresh-a-provider-kubeconfig).

### Use external kcp

For multiple hub replicas, or for a control plane with its own lifecycle, run kcp separately and point the hub at it with a front-proxy kubeconfig. [External kcp with the kcp-operator](https://railgrid.ai/docs/self-hosting/hub/external-kcp/) covers etcd, shards, the front proxy, and minting that kubeconfig. Put it in a Secret in the hub namespace with the key `admin.kubeconfig`:

```bash
kubectl --context HOSTING_CONTEXT -n railgrid-system create secret generic railgrid-kcp \
  --from-file=admin.kubeconfig=/PATH/TO/FRONT-PROXY.kubeconfig
```

Create the namespace first if it does not exist. Protect this kubeconfig as an administrative credential; it is `system:kcp:admin`. The server address and certificate inside it must be reachable and trusted from the hub pods.

Changing `kcp.embedded`/`kcp.external` on an existing embedded installation does not migrate its data; treat that as a separate storage and control-plane migration.

### Settings to review

| Setting | Why it matters |
|---|---|
| `hub.hubExternalURL` | Client-facing origin used by hub flows; match DNS, TLS, and the OIDC redirect URI `<hubExternalURL>/auth/callback`. |
| `hub.internalURL` | In-cluster origin baked into provider kubeconfigs. Must be a SAN of the serving certificate. Leave empty when providers run outside the cluster. |
| `hub.adminUsers` | Platform administrators; empty disables the admin surface and provider onboarding. |
| `hub.trustedProxyCIDRs` | Required behind any proxy or load balancer, or every visitor shares one rate-limit bucket. |
| `hub.portalFrameSources` | Every apps domain whose previews the console embeds, as `https://*.<domain>`. |
| `hub.tls.existingSecret` | The serving certificate, issued from a CA you keep stable. |
| `hub.providerCASecretName` | The trust bundle every minted provider kubeconfig carries: hub CA, plus the kcp server CA for external kcp. |
| `hub.security.providerDelegatedTokens` | `platform` hands providers short-lived workspace-scoped tokens instead of the caller's bearer. Recommended for production. |
| `idp.issuerURL`, `idp.clientID` | Must agree character for character with the issuer's discovery document and the public client registered there. |
| `idp.linkVerifiedEmailIssuers` | Lets returning users keep their account across a connector or issuer change. |
| `kcp.external.existingSecret` | The front-proxy kubeconfig for external kcp, key `admin.kubeconfig`. |
| `replicaCount` | Greater than one requires external kcp. |
| `image.hub.tag` | Defaults to the chart's `appVersion`; pin only when you need a different image. |

For defaults and additional settings, use the [chart values file](https://github.com/railgrid/railgrid/blob/main/deploy/charts/railgrid-hub/values.yaml). Flags the chart does not model go in `hub.extraArgs`; the chart refuses an entry that repeats a flag it already renders.

## Installation sources

The product repo includes executable installation recipes for both modes, run in CI against a fresh kind cluster. Use the recipe and chart from the same revision:

- [Embedded kcp installation](https://github.com/railgrid/railgrid/blob/main/docs/install-embedded-kcp.md)
- [External kcp installation](https://github.com/railgrid/railgrid/blob/main/docs/install-external-kcp.md)

## Verify the installation from the terminal

Use the explicit hosting-cluster context to inspect the discovered workload and serving container. Follow [hosting-cluster diagnostics](https://railgrid.ai/docs/self-hosting/hub/operations/#inspect-the-hosting-cluster-from-the-cli) for pod, rollout, and event commands.

```bash
curl -s https://console.example.com/healthz       # {"status":"ok","oidc":true,...}
kubectl railgrid login --hub-url https://console.example.com
kubectl railgrid use
kubectl api-resources
```

For static-token deployments, follow [login authentication](https://railgrid.ai/docs/reference/cli/login/) instead of expecting a browser flow. Success means authentication works, the intended workspace is selectable, and its APIs are discoverable. Then sign in to the console as one of `adminUsers` and confirm the admin area is available; provider onboarding happens there.

## Next steps

[Install providers](https://railgrid.ai/docs/self-hosting/providers/), then review [operations and recovery](https://railgrid.ai/docs/self-hosting/hub/operations/). For the complete configuration, use the [chart values](https://github.com/railgrid/railgrid/blob/main/deploy/charts/railgrid-hub/values.yaml).
