Nebari integration#
If you are running skillsctl on a cluster managed by Nebari, you can use the NebariApp CRD instead of configuring an Ingress manually. The nebari-operator handles routing, TLS, and OIDC authentication automatically.
Prerequisites#
- nebari-operator v0.1.0-alpha.16 or later, with
KEYCLOAK_EXTERNAL_URLset on the controller deployment (thenicdeploy configures this automatically). Device flow client provisioning requires alpha.16+. - The target namespace must have the
nebari.dev/managed=truelabel (ArgoCD’smanagedNamespaceMetadatahandles this automatically)
What NebariApp does#
When nebariapp.enabled=true, the Helm chart creates a NebariApp resource. The nebari-operator processes that resource and provisions:
- An
HTTPRouteto route traffic to the skillsctl service - A TLS certificate for the hostname via cert-manager
- A confidential Keycloak OIDC client for the server
- A public device flow Keycloak client for CLI authentication
- A Kubernetes Secret with client credentials, issuer URL, and device flow client ID
- RBAC (Role + RoleBinding) scoping Secret access to the app’s ServiceAccount
This means you do not need to configure Ingress, cert-manager, or Keycloak clients manually.
When to use NebariApp vs Ingress#
Use NebariApp when:
- You are deploying to a cluster running the nebari-operator
- You want to reuse the cluster’s Keycloak for authentication
Use ingress.enabled=true when:
- You are on a non-Nebari cluster
- You have your own Ingress controller and TLS setup
- You are using an external OIDC provider
Do not enable both at the same time.
Deploying with ArgoCD#
Most Nebari clusters use ArgoCD for GitOps. See examples/argocd-nebari.yaml in the repository for a complete ArgoCD Application manifest.
The key values:
nebariapp:
enabled: true
hostname: skillsctl.your-domain.com
routing:
routes:
- pathPrefix: /
auth:
enabled: true
provider: keycloak
provisionClient: true
enforceAtGateway: false
deviceFlowClient:
enabled: true
scopes:
- openid
- profile
- emailImportant settings:
routing.routesis required - without it, no HTTPRoute is created and the service is unreachableenforceAtGateway: falselets the server validate bearer tokens directly, which is required for CLI device flow authenticationdeviceFlowClient.enabled: trueprovisions a public Keycloak client forskillsctl auth login
The ArgoCD app should use managedNamespaceMetadata to label the namespace and ServerSideApply=true for compatibility with operator-managed resources:
syncPolicy:
managedNamespaceMetadata:
labels:
nebari.dev/managed: "true"
syncOptions:
- CreateNamespace=true
- ServerSideApply=trueDeploying with Helm directly#
If you prefer Helm over ArgoCD, install from the published Helm repository:
helm repo add nebari https://nebari-dev.github.io/helm-repository/
helm repo update
helm install skillsctl nebari/skillsctl \
--namespace skillsctl --create-namespace \
-f values.yamlOr directly from the OCI registry:
helm install skillsctl oci://quay.io/nebari/charts/skillsctl \
--namespace skillsctl --create-namespace \
-f values.yamlOr from a local checkout:
git clone https://github.com/nebari-dev/skillsctl.git
helm install skillsctl ./skillsctl/chart \
--namespace skillsctl --create-namespace \
-f values.yamlLabel the namespace for the operator:
kubectl label namespace skillsctl nebari.dev/managed=trueOIDC configuration#
When NebariApp is enabled with auth.provisionClient: true, the nebari-operator handles OIDC automatically. You do not need to set oidc.issuerURL or oidc.clientID in your values - the operator writes them to a Kubernetes Secret, and the Helm chart injects them as environment variables.
The server’s /auth/config endpoint returns the issuer URL, client ID, and device flow client ID so the CLI can self-configure. Users just run skillsctl auth login.
Verifying the deployment#
Check that the NebariApp reconciled successfully:
kubectl describe nebariapp skillsctl -n skillsctlAll conditions should be True:
Ready- overall statusTLSReady- certificate provisionedRoutingReady- HTTPRoute createdAuthReady- OIDC clients provisioned
Check the Secret was created with all expected keys:
kubectl get secret skillsctl-oidc-client -n skillsctl -o jsonpath='{.data}' | jq 'keys'Expected: ["client-id", "client-secret", "device-client-id", "issuer-url"]
Check RBAC:
kubectl get role,rolebinding -n skillsctlExpected: skillsctl-oidc-secret-reader Role and RoleBinding.
Test the health endpoint:
curl https://skillsctl.your-domain.com/healthzExpected: ok
Test auth config:
curl https://skillsctl.your-domain.com/auth/configExpected: JSON with enabled: true, issuer_url, client_id, and device_client_id.
Next steps#
- Configuration reference - environment variables
- Auth concepts - how the OIDC device flow works