diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index ce2235a..f7d44fe 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -79,6 +79,9 @@ caddy_services: - name: nvr host: "nvr.{{ caddy_domain }}" backend: "https://nvr.tail8d86e.ts.net" + - name: dex + host: "dex.{{ caddy_domain }}" + backend: "https://dex.tail8d86e.ts.net" - name: ntfy host: "ntfy.{{ caddy_domain }}" backend: "https://ntfy.tail8d86e.ts.net" diff --git a/argocd/apps/dex.yaml b/argocd/apps/dex.yaml new file mode 100644 index 0000000..2da0939 --- /dev/null +++ b/argocd/apps/dex.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: dex + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/dex + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: dex + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/dex/clusterrole.yaml b/argocd/manifests/dex/clusterrole.yaml new file mode 100644 index 0000000..76811df --- /dev/null +++ b/argocd/manifests/dex/clusterrole.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dex +rules: + - apiGroups: ["dex.coreos.com"] + resources: ["*"] + verbs: ["*"] + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["create"] diff --git a/argocd/manifests/dex/clusterrolebinding.yaml b/argocd/manifests/dex/clusterrolebinding.yaml new file mode 100644 index 0000000..53ffefa --- /dev/null +++ b/argocd/manifests/dex/clusterrolebinding.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dex +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dex +subjects: + - kind: ServiceAccount + name: dex + namespace: dex diff --git a/argocd/manifests/dex/deployment.yaml b/argocd/manifests/dex/deployment.yaml new file mode 100644 index 0000000..afddb09 --- /dev/null +++ b/argocd/manifests/dex/deployment.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dex + namespace: dex +spec: + replicas: 1 + selector: + matchLabels: + app: dex + template: + metadata: + labels: + app: dex + spec: + serviceAccountName: dex + containers: + - name: dex + image: registry.ops.eblu.me/blumeops/dex:v1.0.0-nix + ports: + - name: http + containerPort: 5556 + volumeMounts: + - name: config + mountPath: /etc/dex/cfg + readOnly: true + livenessProbe: + httpGet: + path: /healthz + port: 5556 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /healthz + port: 5556 + initialDelaySeconds: 3 + periodSeconds: 10 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + volumes: + - name: config + secret: + secretName: dex-config diff --git a/argocd/manifests/dex/external-secret.yaml b/argocd/manifests/dex/external-secret.yaml new file mode 100644 index 0000000..432e9d3 --- /dev/null +++ b/argocd/manifests/dex/external-secret.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: dex-config + namespace: dex +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: dex-config + creationPolicy: Owner + template: + data: + config.yaml: | + issuer: https://dex.ops.eblu.me + storage: + type: kubernetes + config: + inCluster: true + web: + http: 0.0.0.0:5556 + oauth2: + skipApprovalScreen: true + enablePasswordDB: true + staticPasswords: + - email: "blume.erich@gmail.com" + hash: "{{ .staticPasswordHash }}" + username: "eblume" + userID: "eblume-001" + staticClients: + - id: grafana + name: Grafana + secret: "{{ .grafanaClientSecret }}" + redirectURIs: + - "https://grafana.ops.eblu.me/login/generic_oauth" + - "https://grafana.tail8d86e.ts.net/login/generic_oauth" + data: + - secretKey: staticPasswordHash + remoteRef: + key: "Dex (blumeops)" + property: static-password-hash + - secretKey: grafanaClientSecret + remoteRef: + key: "Dex (blumeops)" + property: grafana-client-secret diff --git a/argocd/manifests/dex/ingress-tailscale.yaml b/argocd/manifests/dex/ingress-tailscale.yaml new file mode 100644 index 0000000..4fc1958 --- /dev/null +++ b/argocd/manifests/dex/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dex-tailscale + namespace: dex + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Dex" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "mdi-shield-key" + gethomepage.dev/description: "OIDC identity provider" + gethomepage.dev/href: "https://dex.ops.eblu.me" + gethomepage.dev/pod-selector: "app=dex" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: dex + port: + number: 5556 + tls: + - hosts: + - dex diff --git a/argocd/manifests/dex/kustomization.yaml b/argocd/manifests/dex/kustomization.yaml new file mode 100644 index 0000000..fea126f --- /dev/null +++ b/argocd/manifests/dex/kustomization.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: dex +resources: + - serviceaccount.yaml + - clusterrole.yaml + - clusterrolebinding.yaml + - external-secret.yaml + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/dex/service.yaml b/argocd/manifests/dex/service.yaml new file mode 100644 index 0000000..f29a20d --- /dev/null +++ b/argocd/manifests/dex/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: dex + namespace: dex +spec: + selector: + app: dex + ports: + - name: http + port: 5556 + targetPort: 5556 diff --git a/argocd/manifests/dex/serviceaccount.yaml b/argocd/manifests/dex/serviceaccount.yaml new file mode 100644 index 0000000..70fc335 --- /dev/null +++ b/argocd/manifests/dex/serviceaccount.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dex + namespace: dex diff --git a/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml b/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml new file mode 100644 index 0000000..c2c4261 --- /dev/null +++ b/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: grafana-dex-oauth + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: grafana-dex-oauth + creationPolicy: Owner + template: + data: + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "{{ .clientSecret }}" + data: + - secretKey: clientSecret + remoteRef: + key: "Dex (blumeops)" + property: grafana-client-secret diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index 18deacc..59e4e19 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -6,6 +6,7 @@ namespace: monitoring resources: - ingress-tailscale.yaml - external-secret-admin.yaml + - external-secret-dex-oauth.yaml - external-secret-teslamate-datasource.yaml # Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1 - dashboards/configmap-borgmatic.yaml diff --git a/argocd/manifests/grafana/values.yaml b/argocd/manifests/grafana/values.yaml index f38cd03..d7c183f 100644 --- a/argocd/manifests/grafana/values.yaml +++ b/argocd/manifests/grafana/values.yaml @@ -12,6 +12,8 @@ admin: envFromSecrets: - name: grafana-teslamate-datasource optional: true + - name: grafana-dex-oauth + optional: true # Persistence with PVC for SQLite database persistence: @@ -28,6 +30,18 @@ grafana.ini: security: # Embedding disabled - iframe approach didn't work well for Homepage allow_embedding: false + auth.generic_oauth: + enabled: true + name: Dex + client_id: grafana + client_secret: $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET} + scopes: openid profile email + auth_url: https://dex.ops.eblu.me/auth + token_url: https://dex.ops.eblu.me/token + api_url: https://dex.ops.eblu.me/userinfo + allow_sign_up: true + role_attribute_path: "'Admin'" + auto_login: false analytics: check_for_updates: false reporting_enabled: false diff --git a/containers/dex/default.nix b/containers/dex/default.nix new file mode 100644 index 0000000..08f6926 --- /dev/null +++ b/containers/dex/default.nix @@ -0,0 +1,28 @@ +# Nix-built Dex OIDC identity provider +# Uses nixpkgs dex-oidc package with Kubernetes CRD storage backend +# Built with dockerTools.buildLayeredImage for efficient layer caching +{ pkgs ? import { } }: + +pkgs.dockerTools.buildLayeredImage { + name = "blumeops/dex"; + tag = "latest"; + + contents = [ + pkgs.dex-oidc + pkgs.cacert + pkgs.tzdata + ]; + + config = { + Entrypoint = [ "${pkgs.dex-oidc}/bin/dex" ]; + Cmd = [ "serve" "/etc/dex/cfg/config.yaml" ]; + Env = [ + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + "TZDIR=${pkgs.tzdata}/share/zoneinfo" + ]; + ExposedPorts = { + "5556/tcp" = { }; + }; + User = "65534"; + }; +} diff --git a/docs/changelog.d/feature-dex-oidc.feature.md b/docs/changelog.d/feature-dex-oidc.feature.md new file mode 100644 index 0000000..a441fad --- /dev/null +++ b/docs/changelog.d/feature-dex-oidc.feature.md @@ -0,0 +1 @@ +Deploy Dex OIDC identity provider on ringtail with Grafana as first SSO client.