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>andui.<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 provider | From the database page |
|---|---|
providerName | Authentication mode (password or iam) |
issuerURL | Database host and port |
clientID | Database name and user |
| Client secret | sslmode |
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.
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:
- Register Hub as a confidential authorization-code client and record the client ID and client secret.
- Set the redirect URI to
https://api.<your-domain>/oidc/callback. It must matchhub-auth.auth.externalURLfrom step 5 followed by/oidc/callback. - Configure the provider to emit group membership in a claim, and note the claim name.
- Create the group you intend to make Hub administrators.
- 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.
- Password authentication
- AWS IAM authentication
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
hub-core:
api:
serviceAccount:
create: true
# IRSA only. Omit this annotation when using EKS Pod Identity.
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hub-core
# The migration Job connects with its own ServiceAccount.
migration:
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hub-core
postgresql:
host: <your-rds-endpoint>
port: 5432
database: hub
user: hub
sslmode: require
auth:
mode: iam
cloud: aws
aws:
region: <your-aws-region>
hub-auth:
auth:
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hub-core
postgresql:
host: <your-rds-endpoint>
port: 5432
database: hub
user: hub
sslmode: require
auth:
mode: iam
cloud: aws
aws:
region: <your-aws-region>
IAM mode needs no password Secret, so step 2 doesn't apply. The chart forces
sslmode=require in this mode and ignores connectionString. Three
ServiceAccounts connect to the database: hub-core, the hub-core migration
Job, and hub-auth. With IRSA, annotate each one and allow each in the role's
trust policy. With EKS Pod Identity, omit the annotations and create an
association for each ServiceAccount. See AWS
RDS for the IAM role, policy, and database-role setup these values
depend on.
Notes on the template:
global.gateway.createcontrols whether the chart owns the Gateway resource. If your platform team already manages a shared Gateway, leavecreate: falseand pointparentRefat it. If you want the chart to render a dedicated Gateway, setcreate: trueand supplygatewayClassName.global.hubCoreExternalUrlmust 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-authdoesn't read it, so itsissuerURLandexternalURLreference the same value through the&hubURLYAML anchor. If you split your values across files, where anchors don't carry over, repeat the URL instead. To givehub-corea different URL, sethub-core.api.externalURL, which overrides the global forhub-coreonly.hub-authserves the OIDC callback, so the redirect URI you registered in step 3 ishub-auth.auth.externalURLfollowed by/oidc/callback. The Gateway routes the/oidcpaths onapi.<your-domain>tohub-auth, so it needs no hostname of its own.hub-core.api.sampleEmailBasedOIDCConfigis a convenience layer over theIdentityProviderresource. It renders one provider for the common case — email as the username, one group claim, and a requiredemail_verified— and sets theauthentication.hub.upbound.io/is-humanextra to"true"for every token it authenticates. It renders only when bothissuerURLandclientIDare set, inline or throughexistingSecretRef. To read the client ID or issuer URL from the Secret as well, add the key to the Secret, setexistingSecretRef.clientIDKeyorexistingSecretRef.issuerURLKey, and remove the matching inline value. Setting both a key and its inline value fails the render.hub-core.bootstrap.adminsacceptsGroupandUserentries. Group names come from your provider's group claim and user names are the user's email, both prefixed with<providerName>:, so a groupplatform-adminsbecomesoidc:platform-admins. From this one list the chart renders anOrganizationRoleBindingbound toorg-adminand aRealmRoleBindingbound torealm-adminon thedefaultrealm.
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.
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:
- The group in your OIDC token matches an entry in
hub-core.bootstrap.admins, including the<providerName>:prefix. - 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.
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.