Flux Web UI SSO with Dex

Flux Operator supports Single Sign-On (SSO) for the Web UI using Dex as the identity provider. Dex is an open-source OIDC provider that can federate multiple identity sources such as GitHub, GitLab, Google, Microsoft, OpenShift and many others. The complete list of supported connectors can be found in the Dex documentation.

Client ID and Secret

First, create a Kubernetes Secret to store the client ID and client secret that Flux Operator will use to authenticate with Dex. We’ll use flux-web as the client ID and generate a random client secret using openssl:

kubectl create secret generic flux-web-client \
  --from-literal=client-id=flux-web \
  --from-literal=client-secret=$(openssl rand -hex 32) \
  -n flux-system

Note that in a production setup, you should either store the Kubernetes Secret encrypted in Git for Flux to sync it, or use an external secret management solution with external-secrets to inject the secret into the cluster.

Flux Operator Configuration

Assuming that you have already deployed the Flux Operator using the Helm chart, you can enable SSO for the Web UI by updating the ResourceSet as follows:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: flux-operator
  namespace: flux-system
spec:
  inputs:
    - domain: "<your-domain>"
      ingressClass: "<your-ingress-class>"
  resources:
    - apiVersion: source.toolkit.fluxcd.io/v1
      kind: OCIRepository
      metadata:
        name: << inputs.provider.name >>
        namespace: << inputs.provider.namespace >>
      spec:
        interval: 30m
        url: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator
        layerSelector:
          mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
          operation: copy
        ref:
          semver: '*'
    - apiVersion: helm.toolkit.fluxcd.io/v2
      kind: HelmRelease
      metadata:
        name: << inputs.provider.name >>
        namespace: << inputs.provider.namespace >>
      spec:
        interval: 30m
        releaseName: << inputs.provider.name >>
        serviceAccountName: << inputs.provider.name >>
        chartRef:
          kind: OCIRepository
          name: << inputs.provider.name >>
        values:
          web:
            config:
              baseURL: "https://flux.<< inputs.domain >>"
              authentication:
                type: OAuth2
                oauth2:
                  provider: OIDC
                  issuerURL: "https://dex.<< inputs.domain >>"
            ingress:
              enabled: true
              className: << inputs.ingressClass >>
              hosts:
                - host: flux.<< inputs.domain >>
                  paths:
                    - path: /
                      pathType: Prefix
              tls:
                - secretName: cluster-tls
                  hosts:
                    - "*.<< inputs.domain >>"
        valuesFrom:
          - kind: Secret
            name: flux-web-client
            valuesKey: client-id
            targetPath: web.config.authentication.oauth2.clientID
          - kind: Secret
            name: flux-web-client
            valuesKey: client-secret
            targetPath: web.config.authentication.oauth2.clientSecret

Make sure to replace the inputs with your actual domain name and Ingress class name. A similar configuration can be applied if you’re exposing the Web UI using Gateway API.

Note that we assume that a wildcard TLS certificate for your domain has been provisioned in the flux-system namespace with the name cluster-tls. You should adjust the configuration according to your TLS setup.

Dex Configuration with Static Users

To deploy Dex as a standalone OIDC provider with a static user:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: dex
  namespace: flux-system
  annotations:
    fluxcd.controlplane.io/reconcileEvery: "30m"
    fluxcd.controlplane.io/reconcileTimeout: "5m"
spec:
  wait: true
  inputs:
    - domain: "<your-domain>"
      ingressClass: "<your-ingress-class>"
  resources:
    - apiVersion: v1
      kind: Namespace
      metadata:
        name: dex
    - apiVersion: v1
      kind: Secret
      metadata:
        name: flux-web-client
        namespace: dex
        annotations:
          fluxcd.controlplane.io/copyFrom: "flux-system/flux-web-client"
    - apiVersion: v1
      kind: Secret
      metadata:
        name: cluster-tls
        namespace: dex
        annotations:
          fluxcd.controlplane.io/copyFrom: "flux-system/cluster-tls"
    - apiVersion: source.toolkit.fluxcd.io/v1
      kind: HelmRepository
      metadata:
        name: dex
        namespace: dex
      spec:
        interval: 1h
        url: https://charts.dexidp.io
    - apiVersion: helm.toolkit.fluxcd.io/v2
      kind: HelmRelease
      metadata:
        name: dex
        namespace: dex
      spec:
        chart:
          spec:
            chart: dex
            version: "*"
            sourceRef:
              kind: HelmRepository
              name: dex
        interval: 1h
        releaseName: dex
        values:
          config:
            issuer: "https://dex.<< inputs.domain >>"
            storage:
              type: memory
            staticClients:
              - id: flux-web
                redirectURIs:
                  - "https://flux.<< inputs.domain >>/oauth2/callback"
            enablePasswordDB: true
            staticPasswords:
              - email: "admin@<< inputs.domain >>"
                # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
                hash: "$2y$10$KR7JHCQ1BxNAKBOR/ixKqevGKtvtZnpgwvV/jF80eN5zLHVHx24E2"
                username: "admin"
                userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
          ingress:
            enabled: true
            className: "<< inputs.ingressClass >>"
            hosts:
              - host: "dex.<< inputs.domain >>"
                paths:
                  - path: /
                    pathType: ImplementationSpecific
            tls:
              - secretName: cluster-tls
                hosts:
                  - "*.<< inputs.domain >>"
        valuesFrom:
          - kind: Secret
            name: flux-web-client
            valuesKey: client-id
            targetPath: config.staticClients[0].name
          - kind: Secret
            name: flux-web-client
            valuesKey: client-secret
            targetPath: config.staticClients[0].secret

Make sure to replace the inputs with your actual domain name and Ingress class name.

After creating the ResourceSet, Dex will be available at https://dex.<your-domain> and configured with a local OIDC provider with a single user (admin@<your-domain> / password).

Users RBAC Configuration

We can now create a ClusterRole and ClusterRoleBinding to grant the admin@<your-domain> user read-only access to all resources in the cluster:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: flux-web-users
  namespace: flux-system
spec:
  inputs:
    - domain: "<your-domain>"
      username: "admin"
  resources:
    - apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRole
      metadata:
        name: flux-web-view
      rules:
        - apiGroups: ["*"]
          resources: ["*"]
          verbs: ["get", "list", "watch"]
    - apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
        name: flux-web-<< inputs.username >>
      subjects:
        - kind: User
          name: << inputs.username >>@<< inputs.domain >>
          apiGroup: rbac.authorization.k8s.io
      roleRef:
        kind: ClusterRole
        name: flux-web-view
        apiGroup: rbac.authorization.k8s.io

Make sure to replace the inputs with your actual domain name.

Apply the ResourceSet and log in to the Flux Web UI using the Dex identity provider with the admin@<your-domain> user. You should have access in the UI to view all resources in the cluster.

Dex Configuration with GitHub

To deploy Dex with a GitHub connector, you need to create a GitHub OAuth App in your GitHub organization. Set the callback URL to https://dex.<your-domain>/callback and note down the client ID and client secret.

Create a Kubernetes Secret to store the GitHub OAuth App credentials:

kubectl create secret generic dex-github-client \
  --from-literal=client-id=<your-github-client-id> \
  --from-literal=client-secret=<your-github-client-secret> \
  -n flux-system

Deploy Dex with the GitHub connector:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: dex
  namespace: flux-system
  annotations:
    fluxcd.controlplane.io/reconcileEvery: "30m"
    fluxcd.controlplane.io/reconcileTimeout: "5m"
spec:
  wait: true
  inputs:
    - domain: "<your-domain>"
      ingressClass: "<your-ingress-class>"
      org: "<your-github-org>"
  resources:
    - apiVersion: v1
      kind: Namespace
      metadata:
        name: dex
    - apiVersion: v1
      kind: Secret
      metadata:
        name: dex-github-client
        namespace: dex
        annotations:
          fluxcd.controlplane.io/copyFrom: "flux-system/dex-github-client"
    - apiVersion: v1
      kind: Secret
      metadata:
        name: flux-web-client
        namespace: dex
        annotations:
          fluxcd.controlplane.io/copyFrom: "flux-system/flux-web-client"
    - apiVersion: v1
      kind: Secret
      metadata:
        name: cluster-tls
        namespace: dex
        annotations:
          fluxcd.controlplane.io/copyFrom: "flux-system/cluster-tls"
    - apiVersion: source.toolkit.fluxcd.io/v1
      kind: HelmRepository
      metadata:
        name: dex
        namespace: dex
      spec:
        interval: 1h
        url: https://charts.dexidp.io
    - apiVersion: helm.toolkit.fluxcd.io/v2
      kind: HelmRelease
      metadata:
        name: dex
        namespace: dex
      spec:
        chart:
          spec:
            chart: dex
            version: "*"
            sourceRef:
              kind: HelmRepository
              name: dex
        interval: 1h
        releaseName: dex
        values:
          config:
            issuer: "https://dex.<< inputs.domain >>"
            storage:
              type: memory
            staticClients:
              - id: flux-web
                redirectURIs:
                  - "https://flux.<< inputs.domain >>/oauth2/callback"
            connectors:
              - type: github
                id: github
                name: GitHub
                config:
                  redirectURI: "https://dex.<< inputs.domain >>/callback"
                  orgs:
                    - name: "<< inputs.org >>"
                  teamNameField: slug
          ingress:
            enabled: true
            className: "<< inputs.ingressClass >>"
            hosts:
              - host: "dex.<< inputs.domain >>"
                paths:
                  - path: /
                    pathType: ImplementationSpecific
            tls:
              - secretName: cluster-tls
                hosts:
                  - "*.<< inputs.domain >>"
        valuesFrom:
          - kind: Secret
            name: flux-web-client
            valuesKey: client-id
            targetPath: config.staticClients[0].name
          - kind: Secret
            name: flux-web-client
            valuesKey: client-secret
            targetPath: config.staticClients[0].secret
          - kind: Secret
            name: dex-github-client
            valuesKey: client-id
            targetPath: config.connectors[0].config.clientID
          - kind: Secret
            name: dex-github-client
            valuesKey: client-secret
            targetPath: config.connectors[0].config.clientSecret

Make sure to set the inputs to your actual domain name, Ingress class, and GitHub organization.

It is recommended to configure Dex with persistent storage to avoid losing user sessions on restarts. On production systems, consider decreasing the tokens expiry which defaults to 24 hours, as the ID token holds the session RBAC permissions.

Group RBAC Configuration

Assuming that your GitHub organization has a team named admins, you can create a ClusterRoleBinding to grant cluster wide access to members of that team:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: flux-web-admins
  namespace: flux-system
spec:
  inputs:
    - org: "<your-github-org>"
      team: "admin"
  resources:
    - apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
        name: flux-web-<< inputs.team >>
      subjects:
        - kind: Group
          name: "<< inputs.org >>:<< inputs.team >>"
          apiGroup: rbac.authorization.k8s.io
      roleRef:
        kind: ClusterRole
        name: admin
        apiGroup: rbac.authorization.k8s.io

Assuming that your GitHub organization has dev teams that should have access to resources in their respective namespaces:

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: flux-web-devs
  namespace: flux-system
spec:
  inputs:
    - org: "<your-github-org>"
      team: "dev-team-1"
      namespace: "apps-1"
    - org: "<your-github-org>"
      team: "dev-team-2"
      namespace: "apps-2"
  resources:
    - apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
        name: flux-web-<< inputs.team >>
        namespace: "<< inputs.namespace >>"
      subjects:
        - kind: Group
          name: "<< inputs.org >>:<< inputs.team >>"
          apiGroup: rbac.authorization.k8s.io
      roleRef:
        kind: ClusterRole
        name: admin
        apiGroup: rbac.authorization.k8s.io

The above example assumes that you have two dev teams (dev-team-1 and dev-team-2) that should have access to resources in the apps-1 and apps-2 namespaces respectively.

Further Reading

Dex supports a wide range of identity providers and connectors, to use GitLab, Google, Microsoft or others, refer to the Dex documentation.