Skip to main content
Version: 1.2

Installing Hub

A self-hosted Hub install needs a PostgreSQL database, an OIDC provider, and ingress routes for the Hub services.

This page owns the values used to install the hub Helm chart. The previous pages set up those dependencies and tell you which values to record.

Prerequisites​

Before starting, work through the prerequisites page and confirm each item is ready. This page assumes you have:

  • A Kubernetes cluster with a Gateway API or Ingress controller installed and a real CA-signed TLS certificate for the hostnames you intend to use
  • DNS records for the hostnames api.<your-domain> and ui.<your-domain> pointing at the cluster's load balancer or Gateway address
  • A provisioned PostgreSQL database, following the databases overview and the provider page it links to
  • An OIDC-compliant identity provider you can register Hub with, and admin access to it. Step 3 walks the registration; Upbound Identity explains what Hub does with the resulting tokens

Have the values from both of those pages in front of you before you start. Between them they supply every placeholder in step 5:

From your OIDC providerFrom the database page
providerNameAuthentication mode (password or iam)
issuerURLDatabase host and port
clientIDDatabase name and user
Client secretsslmode
groupsClaim (if your provider deviates)Password secret name and key, or AWS region and role ARN
Admin group or user names

The chart reference​

Hub installs from an umbrella chart at this OCI reference:

oci://xpkg.upbound.io/upbound/hub

Helm resolves the latest release when you don't pass --version. Add --version <version> to pin a release, which is what you want in a production pipeline so an upgrade is a deliberate change rather than a side effect of running the command again.

Install​

1. Create the namespace​

kubectl create namespace hub

The rest of this guide installs all Hub resources into hub. Substitute your own namespace name if you prefer.

2. Create the Postgres credentials secret​

Skip this step if you chose IAM authentication on the database page. IAM mode stores no password, so it needs no Secret.

For password authentication, create a Secret holding the database password under the key password:

kubectl -n hub create secret generic hub-core-postgres \
--from-literal=password='<your-postgres-password>'

You reference this Secret from values.yaml in step 5 via hub-core.postgresql.auth.password.existingSecretRef.

note

Hub doesn't require the Secret name hub-core-postgres. Any Secret in the same namespace as the release works as long as the name and key match the existingSecretRef fields you set in values.

3. Register Hub with your OIDC provider​

Do this in your provider's console, before touching Helm. Each provider emits group claims its own way, so follow its page for the specifics: Microsoft Entra ID (groups need explicit app-registration configuration and arrive as object IDs), Amazon Cognito (groups arrive under cognito:groups), Google Workspace (no group claim at all — bind users by email, or inject groups upstream), Keycloak (standards-compliant with the group-membership mapper enabled), or Okta (standards-compliant once you add a groups claim to the authorization server).

Whichever you use:

  1. Register Hub as a confidential authorization-code client and record the client ID and client secret.
  2. Set the redirect URI to https://api.<your-domain>/oidc/callback. It must match hub-auth.auth.externalURL from step 5 followed by /oidc/callback.
  3. Configure the provider to emit group membership in a claim, and note the claim name.
  4. Create the group you intend to make Hub administrators.
  5. Record the issuer URL from the provider's discovery document.

4. Create the OIDC client-secret secret​

Keep the OIDC client secret out of your values.yaml file (and out of source control) by storing it in a Kubernetes Secret in the release namespace. The chart reads it from there, so no credential appears in Helm values. The key client-secret is the default the chart looks for:

kubectl -n hub create secret generic hub-oidc \
--from-literal=client-secret='<your-oidc-client-secret>'

You can manage this Secret with External Secrets, Sealed Secrets, or any other tool you already use. hub-auth can read every key in the Secret, so keep only OIDC values in it.

In step 5 you name this Secret in two places: hub-core.api.sampleEmailBasedOIDCConfig.existingSecretRef.name and hub-auth.auth.bootstrap.credentialSecretRefs. hub-core renders the identity provider that refers to the Secret and hub-auth reads it, so the chart fails to render when hub-auth doesn't list the Secret hub-core names.

A rotated client secret takes effect within a few minutes, or as soon as you restart hub-auth.

5. Assemble values.yaml​

Save the following as values.yaml. It carries the ingress, OIDC, and hub-auth settings, and you complete it with the Postgres blocks for the authentication mode you chose. The template includes only the keys you need to change for a self-hosted install. Defaults cover everything else.

global:
# Externally reachable base URL for Hub. hub-core, the Console's
# login redirect, and in-cluster connectors all read it, so set it
# once here. The &hubURL anchor lets hub-auth reuse it below.
hubCoreExternalUrl: &hubURL https://api.<your-domain>
gateway:
enabled: true
# Set to true if you want this chart to render the Gateway
# resource. Leave false when you manage the Gateway separately
# and only want HTTPRoutes attached to it.
create: false
# Required when create=true. Identifies the GatewayClass your
# cluster's controller has registered (for example "envoy",
# "istio", "cilium").
gatewayClassName: ""
# Apex domain Hub is served from. Subcharts compose
# <subdomain>.<domain> hostnames from this value.
domain: <your-domain>
# parentRef points at an existing Gateway. Remove these values
# below if you set create=true.
parentRef:
name: <your-gateway-name>
namespace: <your-gateway-namespace>
listeners:
https:
enabled: true
port: 443
tls:
mode: Terminate
# TLS certificate Hub serves on api.<your-domain> and
# ui.<your-domain>. Reference a Secret of type
# kubernetes.io/tls in the Gateway's namespace.
certificateRefs:
- kind: Secret
name: <your-tls-secret>

hub-core:
api:
# Values recorded in step 3. Renders a complete IdentityProvider
# for you; see the notes below the template.
sampleEmailBasedOIDCConfig:
# Prefixes every username and group Hub derives from this
# provider, as "<providerName>:". Immutable once installed.
providerName: oidc
issuerURL: <your-oidc-issuer-url>
clientID: <your-oidc-client-id>
# Reads the client secret from the Secret created in step 4.
# hub-auth.auth.bootstrap.credentialSecretRefs must list the
# same Secret.
existingSecretRef:
name: hub-oidc
# Optional. Restrict logins to a single email domain.
allowedDomain: <your-email-domain>
# Defaults to "groups". Amazon Cognito needs "cognito:groups".
groupsClaim: groups

# First administrators. Without at least one entry here, the first
# user to log in has no permissions and no way to grant any.
bootstrap:
admins:
- kind: Group
name: "oidc:<your-admin-group-name>"

# hub-auth serves OIDC login, token exchange, and the signing keys
# hub-core verifies tokens against. The chart doesn't deploy it
# unless you enable it.
hub-auth:
enabled: true
auth:
# hub-auth doesn't read global.hubCoreExternalUrl, so both URLs
# reference it through the anchor.
# Issuer hub-auth stamps on hub-issued tokens. hub-core expects
# this issuer, so it must equal the external URL.
issuerURL: *hubURL
# Base of the OIDC callback URI (<externalURL>/oidc/callback).
# Must match the redirect URI registered with your provider.
externalURL: *hubURL
# Where the browser lands after login. Empty redirects to "/"
# on the API hostname.
frontendURL: https://ui.<your-domain>
bootstrap:
# Must list the Secret named in
# hub-core.api.sampleEmailBasedOIDCConfig.existingSecretRef.
credentialSecretRefs:
- name: hub-oidc

Now add the Postgres blocks for your authentication mode. hub-core and hub-auth each read their own database settings, so point both at the same database. Merge each block into the matching hub-core: and hub-auth: keys above.

hub-core:
postgresql:
# Either set connectionString OR host + database + user + auth.
# Leave connectionString empty to use the structured form below.
connectionString: ""
host: <your-postgres-host>
port: 5432
database: hub
user: hub
# Set to require (or stricter) when your database enforces TLS.
sslmode: require
auth:
mode: password
password:
# Reference to the Secret created in step 2.
existingSecretRef:
name: hub-core-postgres
key: password

hub-auth:
postgresql:
host: <your-postgres-host>
port: 5432
database: hub
user: hub
sslmode: require
auth:
mode: password
password:
existingSecretRef:
name: hub-core-postgres
key: password

Notes on the template:

  • global.gateway.create controls whether the chart owns the Gateway resource. If your platform team already manages a shared Gateway, leave create: false and point parentRef at it. If you want the chart to render a dedicated Gateway, set create: true and supply gatewayClassName.
  • global.hubCoreExternalUrl must match the hostname clients (browsers and CLI tools) use to reach Hub's API. hub-core, the Console, and in-cluster connectors read it, so you set it once. hub-auth doesn't read it, so its issuerURL and externalURL reference the same value through the &hubURL YAML anchor. If you split your values across files, where anchors don't carry over, repeat the URL instead. To give hub-core a different URL, set hub-core.api.externalURL, which overrides the global for hub-core only.
  • hub-auth serves the OIDC callback, so the redirect URI you registered in step 3 is hub-auth.auth.externalURL followed by /oidc/callback. The Gateway routes the /oidc paths on api.<your-domain> to hub-auth, so it needs no hostname of its own.
  • hub-core.api.sampleEmailBasedOIDCConfig is a convenience layer over the IdentityProvider resource. It renders one provider for the common case — email as the username, one group claim, and a required email_verified — and sets the authentication.hub.upbound.io/is-human extra to "true" for every token it authenticates. It renders only when both issuerURL and clientID are set, inline or through existingSecretRef. To read the client ID or issuer URL from the Secret as well, add the key to the Secret, set existingSecretRef.clientIDKey or existingSecretRef.issuerURLKey, and remove the matching inline value. Setting both a key and its inline value fails the render.
  • hub-core.bootstrap.admins accepts Group and User entries. Group names come from your provider's group claim and user names are the user's email, both prefixed with <providerName>:, so a group platform-admins becomes oidc:platform-admins. From this one list the chart renders an OrganizationRoleBinding bound to org-admin and a RealmRoleBinding bound to realm-admin on the default realm.
warning

Without at least one entry in hub-core.bootstrap.admins, the first user to log in has no permissions and no way to grant themselves any. Set it before the first install.

Reach past the sample layer and write a full IdentityProvider when you need more than one provider, a username that isn't the email claim, directory search, validation beyond email_verified and allowedDomain, or issuer plumbing such as a private CA bundle or an in-cluster backendIssuerURL. Supply it through hub-core.bootstrap.files. Any file name adds a provider alongside the generated one. Naming your file oidc-idp.yaml replaces the generated provider instead, but only when sampleEmailBasedOIDCConfig.existingSecretRef is unset. With it set, the chart refuses to render, so remove it first. Bootstrap files are reapplied every five minutes, so they stay the source of truth over changes made through the API.

note

The chart exposes many more values than the ones shown here. See the values reference for the full surface. Anything not set in values.yaml falls back to the chart default.

6. Install the chart​

Install the chart with values.yaml:

helm install hub oci://xpkg.upbound.io/upbound/hub \
--namespace hub \
--values values.yaml

The chart runs the database migrations in a Job. Each hub-core Pod's init-migrate init container waits until the schema has every migration the release ships before the main container starts. Wait for the migration Job to complete, then for the Deployments to roll out:

kubectl -n hub wait --for=condition=complete job \
-l app.kubernetes.io/component=api-migrate --timeout=5m
kubectl -n hub rollout status deployment/hub-core
kubectl -n hub rollout status deployment/hub-auth
kubectl -n hub rollout status deployment/hub-webui

The migration Job's Pod ends in the Completed state and never reports Ready, so don't wait on condition=ready for every Pod in the namespace.

Verify the Install​

Confirm Hub is up and you can reach it before registering any control planes.

Check that all Hub Pods are Ready:

kubectl -n hub get pods

You should see Ready replicas for hub-core, hub-core-aggregator (which serves API discovery), hub-auth, and hub-webui, and a Completed Pod for the hub-core-migrate-* Job. The umbrella chart includes the hub-connector Deployment, but hub-core itself doesn't use it. It activates only when you install a connector on an observed cluster.

Open https://ui.<your-domain> in a browser. The browser should redirect to your OIDC provider for login, then returned to the Hub UI.

Confirm admin access​

Signing in as a member of the group you listed in hub-core.bootstrap.admins gives you organization and realm admin rights immediately. The chart rendered both bindings during install.

If login completes but the UI shows "no permissions", confirm:

  1. The group in your OIDC token matches an entry in hub-core.bootstrap.admins, including the <providerName>: prefix.
  2. Your OIDC provider is sending the group claim. Inspect the JWT at the provider's debug endpoint or in your browser's network tab.

To grant access beyond these first administrators, see RBAC and OIDC group mapping.

A fresh install has no registered control planes. The next section adds one.

Configure​

With admin access confirmed, register your first ControlPlane and mint a registration token for its connector.

Register the first control plane​

Apply a ControlPlane resource for the cluster (or clusters) you register a hub-connector against. The example below registers a production ControlPlane in the default realm and trusts your OIDC IdentityProvider for user lookups on that ControlPlane.

Save as bootstrap-controlplane.yaml:

apiVersion: fleet.hub.upbound.io/v1beta1
kind: ControlPlane
metadata:
name: production
namespace: default
spec:
identityProviders:
- name: oidc
userMappingType: Exact

Apply it:

kubectl apply -f bootstrap-controlplane.yaml

The spec.identityProviders[].name value must match the providerName you set in hub-core.api.sampleEmailBasedOIDCConfig.providerName. Hub uses that IdentityProvider to resolve users referenced by role bindings on this ControlPlane.

Mint a registration token​

After you create the ControlPlane, mint a registration token for it. The token is what hub-connector presents when it first contacts hub-core:

REGISTRATION_TOKEN=$(echo '{"apiVersion":"fleet.hub.upbound.io/v1beta1","kind":"ControlPlane"}' \
| kubectl create --raw \
"/apis/fleet.hub.upbound.io/v1beta1/namespaces/default/controlplanes/production/registrationtoken" \
-f - | jq -r '.status.registrationToken')

The registrationtoken subresource returns the token in .status.registrationToken. Store it somewhere safe. You can't read it back later, but you can mint a new one at any time. See Connect a control plane for the full flow. You hand it to the hub-connector install on the observed cluster. The standalone connector install pattern carries over here, with the connector's connector.hub.url pointing at your gateway instead of a Docker-network address.

note

The chart also accepts hub-core.bootstrap.files for delivering ControlPlanes and other Hub objects as part of the Helm release. Use it when you want the control plane inventory version-controlled alongside the release rather than applied by hand.

Refresh https://ui.<your-domain>. The production ControlPlane appears in the ControlPlane list, with no resources yet since it has no connector attached.

Next step​

You have a working self-hosted Hub. Before letting traffic that matters depend on it, work through the production overview for sizing, high availability, autoscaling, RBAC, and upgrade guidance.

To attach a control plane, install the standalone hub-connector chart on the observed cluster with the registration token you minted above. For self-hosted installs the connector's connector.hub.url and connector.hub.tokenExchangeUrl point at the public hostname of your gateway instead.