# 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 <a href="https://dexidp.io/docs/connectors/" target="_blank" rel="noopener noreferrer">Dex documentation</a>.

## 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`:

```bash
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](/docs/crd/resourceset/) as follows:

```yaml
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](/docs/web-ui/ingress/#gateway-api-configuration).

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:

```yaml
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:

```yaml
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: 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-admin
        apiGroup: rbac.authorization.k8s.io
```

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

Apply the [ResourceSet](/docs/crd/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.

Note that the `flux-web-admin` is a predefined role included with the Flux Operator
that grants full access to Flux resources including the ability to perform actions.
See the [user management RBAC](/docs/web-ui/user-management/#role-based-access-control)
section for more information about predefined roles.

## 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:

```bash
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:

```yaml
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 <a href="https://dexidp.io/docs/configuration/storage/" target="_blank" rel="noopener noreferrer">persistent storage</a> 
to avoid losing user sessions on restarts. On production systems, consider decreasing
the <a href="https://dexidp.io/docs/configuration/tokens/" target="_blank" rel="noopener noreferrer">tokens expiry</a> 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:

```yaml
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: flux-web-admins
  namespace: flux-system
spec:
  inputs:
    - org: "<your-github-org>"
      team: "admins"
  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: flux-web-admin
        apiGroup: rbac.authorization.k8s.io
```

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

```yaml
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: flux-web-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 <a href="https://dexidp.io/docs/connectors/" target="_blank" rel="noopener noreferrer">Dex documentation</a>.