From 45aa5bd20e7674bafc5a3d2b9640c238e9071f0e Mon Sep 17 00:00:00 2001 From: Masaki Yatsu Date: Sat, 13 Sep 2025 00:15:31 +0900 Subject: [PATCH] feat(oauth2-proxy) add oauth2-proxy module --- justfile | 1 + oauth2-proxy/justfile | 130 ++++++++++++++++++ .../oauth2-proxy-deployment.gomplate.yaml | 78 +++++++++++ ...oauth2-proxy-external-secret.gomplate.yaml | 32 +++++ .../oauth2-proxy-ingressroute.gomplate.yaml | 36 +++++ .../oauth2-proxy-service.gomplate.yaml | 15 ++ 6 files changed, 292 insertions(+) create mode 100644 oauth2-proxy/justfile create mode 100644 oauth2-proxy/oauth2-proxy-deployment.gomplate.yaml create mode 100644 oauth2-proxy/oauth2-proxy-external-secret.gomplate.yaml create mode 100644 oauth2-proxy/oauth2-proxy-ingressroute.gomplate.yaml create mode 100644 oauth2-proxy/oauth2-proxy-service.gomplate.yaml diff --git a/justfile b/justfile index f103d8d..b9845e0 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,7 @@ mod k8s mod longhorn mod metabase mod minio +mod oauth2-proxy mod postgres mod utils mod vault diff --git a/oauth2-proxy/justfile b/oauth2-proxy/justfile new file mode 100644 index 0000000..2b9093f --- /dev/null +++ b/oauth2-proxy/justfile @@ -0,0 +1,130 @@ +set fallback := true + +export OAUTH2_PROXY_NAMESPACE := env("OAUTH2_PROXY_NAMESPACE", "default") +export KEYCLOAK_HOST := env("KEYCLOAK_HOST", "") +export KEYCLOAK_REALM := env("KEYCLOAK_REALM", "buunstack") +export OAUTH2_PROXY_HOST := env("OAUTH2_PROXY_HOST", "") +export EXTERNAL_SECRETS_NAMESPACE := env("EXTERNAL_SECRETS_NAMESPACE", "external-secrets") + +[private] +default: + @just --list --unsorted --list-submodules + +# Setup OAuth2 Proxy for an application +setup-for-app app_name app_host app_namespace="default" upstream_service="": + #!/bin/bash + set -euo pipefail + + echo "Setting up OAuth2 Proxy for {{ app_name }}" + + # Create Keycloak client + echo "Creating Keycloak client for {{ app_name }}..." + client_id="{{ app_name }}-oauth2-proxy" + redirect_url="https://{{ app_host }}/oauth2/callback" + + # Generate client secret for confidential client + client_secret=$(just utils::random-password 32) + + if ! just keycloak::create-client "${KEYCLOAK_REALM}" "${client_id}" "${redirect_url}" "${client_secret}"; then + echo "Failed to create Keycloak client" + exit 1 + fi + + # Add audience mapper to Keycloak client + echo "Adding audience mapper to Keycloak client..." + just keycloak::add-audience-mapper "${client_id}" + + # Generate cookie secret + cookie_secret=$(just utils::random-password 32) + + # Create namespace if it doesn't exist + kubectl get namespace {{ app_namespace }} &>/dev/null || \ + kubectl create namespace {{ app_namespace }} + + # Store secrets + if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then + echo "External Secrets Operator detected. Storing credentials in Vault..." + + just vault::put "oauth2-proxy/{{ app_name }}" \ + client_id="${client_id}" \ + client_secret="${client_secret}" \ + cookie_secret="${cookie_secret}" + + # Create ExternalSecret + export APP_NAME="{{ app_name }}" + export APP_HOST="{{ app_host }}" + export APP_NAMESPACE="{{ app_namespace }}" + gomplate -f oauth2-proxy-external-secret.gomplate.yaml | kubectl apply -f - + + echo "Waiting for ExternalSecret to sync..." + kubectl wait --for=condition=Ready externalsecret/oauth2-proxy-{{ app_name }}-config \ + -n {{ app_namespace }} --timeout=60s + else + echo "Creating Kubernetes secret directly..." + kubectl create secret generic oauth2-proxy-{{ app_name }}-config \ + -n {{ app_namespace }} \ + --from-literal=client_id="${client_id}" \ + --from-literal=client_secret="${client_secret}" \ + --from-literal=cookie_secret="${cookie_secret}" \ + --dry-run=client -o yaml | kubectl apply -f - + fi + + # Set upstream service (default to static response if not provided) + if [ -z "{{ upstream_service }}" ]; then + upstream_service="static://202" + else + upstream_service="{{ upstream_service }}" + fi + + # Deploy OAuth2 Proxy + export APP_NAME="{{ app_name }}" + export APP_HOST="{{ app_host }}" + export APP_NAMESPACE="{{ app_namespace }}" + export UPSTREAM_SERVICE="${upstream_service}" + + gomplate -f oauth2-proxy-deployment.gomplate.yaml | kubectl apply -f - + gomplate -f oauth2-proxy-service.gomplate.yaml | kubectl apply -f - + gomplate -f oauth2-proxy-ingressroute.gomplate.yaml | kubectl apply -f - + + echo "Waiting for OAuth2 Proxy to be ready..." + kubectl wait --for=condition=Available deployment/oauth2-proxy-{{ app_name }} \ + -n {{ app_namespace }} --timeout=120s + + echo "OAuth2 Proxy setup completed for {{ app_name }}" + echo "Access URL: https://{{ app_host }}/oauth2/sign_in" + +# Remove OAuth2 Proxy for an application +remove-for-app app_name app_namespace="default": + #!/bin/bash + set -euo pipefail + + echo "Removing OAuth2 Proxy for {{ app_name }}" + + # Delete Kubernetes resources + kubectl delete ingressroute oauth2-proxy-{{ app_name }} -n {{ app_namespace }} --ignore-not-found + kubectl delete service oauth2-proxy-{{ app_name }} -n {{ app_namespace }} --ignore-not-found + kubectl delete deployment oauth2-proxy-{{ app_name }} -n {{ app_namespace }} --ignore-not-found + kubectl delete configmap oauth2-proxy-{{ app_name }}-config -n {{ app_namespace }} --ignore-not-found + kubectl delete secret oauth2-proxy-{{ app_name }}-config -n {{ app_namespace }} --ignore-not-found + kubectl delete externalsecret oauth2-proxy-{{ app_name }}-config -n {{ app_namespace }} --ignore-not-found + + # Remove Vault secrets if External Secrets is available + if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then + just vault::delete "oauth2-proxy/{{ app_name }}" + fi + + # Delete Keycloak client + client_id="{{ app_name }}-oauth2-proxy" + just keycloak::delete-client "${KEYCLOAK_REALM}" "${client_id}" + + echo "OAuth2 Proxy removed for {{ app_name }}" + +# List OAuth2 Proxy deployments +list: + @echo "OAuth2 Proxy deployments:" + @kubectl get deployments -A -l app.kubernetes.io/component=oauth2-proxy + +# Show OAuth2 Proxy status for an application +status app_name app_namespace="default": + @echo "OAuth2 Proxy status for {{ app_name }}:" + @kubectl get deployment,service,ingressroute -n {{ app_namespace }} -l app={{ app_name }}-oauth2-proxy diff --git a/oauth2-proxy/oauth2-proxy-deployment.gomplate.yaml b/oauth2-proxy/oauth2-proxy-deployment.gomplate.yaml new file mode 100644 index 0000000..b238bde --- /dev/null +++ b/oauth2-proxy/oauth2-proxy-deployment.gomplate.yaml @@ -0,0 +1,78 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + namespace: {{ .Env.APP_NAMESPACE }} +data: + config.cfg: | + http_address = "0.0.0.0:4180" + provider = "keycloak-oidc" + oidc_issuer_url = "https://{{ .Env.KEYCLOAK_HOST }}/realms/{{ .Env.KEYCLOAK_REALM }}" + redirect_url = "https://{{ .Env.APP_HOST }}/oauth2/callback" + email_domains = "*" + reverse_proxy = true + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oauth2-proxy-{{ .Env.APP_NAME }} + namespace: {{ .Env.APP_NAMESPACE }} + labels: + app: {{ .Env.APP_NAME }}-oauth2-proxy + app.kubernetes.io/component: oauth2-proxy +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Env.APP_NAME }}-oauth2-proxy + template: + metadata: + labels: + app: {{ .Env.APP_NAME }}-oauth2-proxy + app.kubernetes.io/component: oauth2-proxy + spec: + containers: + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + args: + - --config=/etc/oauth2-proxy/config.cfg + - --upstream=http://{{ .Env.UPSTREAM_SERVICE }} + env: + - name: OAUTH2_PROXY_CLIENT_ID + valueFrom: + secretKeyRef: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + key: client_id + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + key: client_secret + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + key: cookie_secret + ports: + - containerPort: 4180 + name: http + volumeMounts: + - name: config + mountPath: /etc/oauth2-proxy/ + readinessProbe: + httpGet: + path: /ping + port: 4180 + initialDelaySeconds: 3 + timeoutSeconds: 1 + livenessProbe: + httpGet: + path: /ping + port: 4180 + initialDelaySeconds: 3 + timeoutSeconds: 1 + volumes: + - name: config + configMap: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config \ No newline at end of file diff --git a/oauth2-proxy/oauth2-proxy-external-secret.gomplate.yaml b/oauth2-proxy/oauth2-proxy-external-secret.gomplate.yaml new file mode 100644 index 0000000..aed41e1 --- /dev/null +++ b/oauth2-proxy/oauth2-proxy-external-secret.gomplate.yaml @@ -0,0 +1,32 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + namespace: {{ .Env.APP_NAMESPACE }} +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-secret-store + kind: ClusterSecretStore + target: + name: oauth2-proxy-{{ .Env.APP_NAME }}-config + creationPolicy: Owner + template: + type: Opaque + data: + client_id: "{{ `{{ .client_id }}` }}" + client_secret: "{{ `{{ .client_secret }}` }}" + cookie_secret: "{{ `{{ .cookie_secret }}` }}" + data: + - secretKey: client_id + remoteRef: + key: oauth2-proxy/{{ .Env.APP_NAME }} + property: client_id + - secretKey: client_secret + remoteRef: + key: oauth2-proxy/{{ .Env.APP_NAME }} + property: client_secret + - secretKey: cookie_secret + remoteRef: + key: oauth2-proxy/{{ .Env.APP_NAME }} + property: cookie_secret \ No newline at end of file diff --git a/oauth2-proxy/oauth2-proxy-ingressroute.gomplate.yaml b/oauth2-proxy/oauth2-proxy-ingressroute.gomplate.yaml new file mode 100644 index 0000000..9d143de --- /dev/null +++ b/oauth2-proxy/oauth2-proxy-ingressroute.gomplate.yaml @@ -0,0 +1,36 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ .Env.APP_NAME }}-auth-headers + namespace: {{ .Env.APP_NAMESPACE }} +spec: + headers: + sslRedirect: true + stsSeconds: 315360000 + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + sslHost: {{ .Env.APP_HOST }} + stsIncludeSubdomains: true + stsPreload: true + frameDeny: true + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: oauth2-proxy-{{ .Env.APP_NAME }} + namespace: {{ .Env.APP_NAMESPACE }} + labels: + app: {{ .Env.APP_NAME }}-oauth2-proxy +spec: + entryPoints: + - websecure + routes: + - match: "Host(`{{ .Env.APP_HOST }}`)" + kind: Rule + services: + - name: oauth2-proxy-{{ .Env.APP_NAME }} + port: 80 + middlewares: + - name: {{ .Env.APP_NAME }}-auth-headers \ No newline at end of file diff --git a/oauth2-proxy/oauth2-proxy-service.gomplate.yaml b/oauth2-proxy/oauth2-proxy-service.gomplate.yaml new file mode 100644 index 0000000..2863292 --- /dev/null +++ b/oauth2-proxy/oauth2-proxy-service.gomplate.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: oauth2-proxy-{{ .Env.APP_NAME }} + namespace: {{ .Env.APP_NAMESPACE }} + labels: + app: {{ .Env.APP_NAME }}-oauth2-proxy +spec: + ports: + - port: 80 + targetPort: 4180 + protocol: TCP + name: http + selector: + app: {{ .Env.APP_NAME }}-oauth2-proxy \ No newline at end of file