From 2836c22e3b151de268e708946dab2e129daa3a76 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Fri, 8 Aug 2025 20:52:06 +1000 Subject: [PATCH] Revert "Merge pull request #9 from wahyd4/traefik-support" This reverts commit 01a625d8fdbef0f0222b746eb11b27057f043137, reversing changes made to 1aba3b394702ef0e7a39d8e7c36ac5525e0363db. --- .github/workflows/ci.yml | 2 +- .gitignore | 1 - Makefile | 12 ++ README.md | 68 +----- helm/passkey-auth/Chart.yaml | 5 +- helm/passkey-auth/README.md | 48 +---- .../passkey-auth/examples/values-traefik.yaml | 55 ----- .../templates/traefik-middleware.yaml | 27 --- helm/passkey-auth/values.yaml | 7 - internal/handlers/handlers.go | 44 +--- internal/handlers/handlers_test.go | 195 ------------------ main.go | 4 +- 12 files changed, 27 insertions(+), 441 deletions(-) delete mode 100644 helm/passkey-auth/examples/values-traefik.yaml delete mode 100644 helm/passkey-auth/templates/traefik-middleware.yaml delete mode 100644 internal/handlers/handlers_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42c85bb..ebf7991 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: latest - args: --timeout=5m --tests=false + args: --timeout=5m security: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 4428957..c524132 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,3 @@ dev-* web/test.html *.log -test*.yaml diff --git a/Makefile b/Makefile index 0f118d7..529278e 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,18 @@ docker-run: docker-build ## Run with Docker Compose @docker-compose up -d @echo "Access the application at http://localhost:8080" +docker-stop: ## Stop Docker Compose + @echo "πŸ›‘ Stopping Docker Compose..." + @docker-compose down + +k8s-deploy: docker-build ## Deploy to Kubernetes + @echo "☸️ Deploying to Kubernetes..." + @./scripts/deploy.sh + +k8s-undeploy: ## Remove from Kubernetes + @echo "☸️ Removing from Kubernetes..." + @./scripts/undeploy.sh + deps: ## Download dependencies @echo "πŸ“¦ Downloading dependencies..." @go mod download diff --git a/README.md b/README.md index 7616f89..e30e4a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# πŸ” Passkey Auth for Kubernetes Ingress Controllers +# πŸ” Passkey Auth for Kubernetes Nginx Ingress -A WebAuthn-based passkey authentication provider that integrates with ingress controllers. Currently supports Kubernetes Nginx Ingress Controller and Traefik Ingress Controller. Provides secure, passwordless authentication using passkeys (FIDO2/WebAuthn) as an auth backend. +A WebAuthn-based passkey authentication provider that integrates ingress controllers, currently support Kubernetes nginx Ingress controller. Provides secure, passwordless authentication using passkeys (FIDO2/WebAuthn) as an auth backend for nginx ingress. ## TLDR; @@ -17,7 +17,7 @@ I use it for signing into my home lab apps. ## ✨ Features - **Passwordless Authentication**: Uses WebAuthn/FIDO2 passkeys for secure authentication -- **Ingress Controller Integration**: Works as auth backend for Nginx Ingress (`auth_request`) and Traefik Ingress (`ForwardAuth`) +- **Nginx Ingress Integration**: Works as auth backend using nginx `auth_request` directive - **User Management**: An simple Admin interface for managing users and approval status - **Kubernetes Native**: Designed for Kubernetes deployment with persistent storage @@ -63,17 +63,15 @@ go run main.go ### Setup Your App's Ingress -#### Nginx Ingress Controller - ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: your-app-ingress annotations: + nginx.ingress.kubernetes.io/auth-url: "https://your-passkey-auth.com/auth" nginx.ingress.kubernetes.io/auth-signin: "https://your-passkey-auth.com/?redirect=https%3A%2F%2F$host$request_uri" - nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-User,X-Auth-Email" spec: rules: - host: your-app.com @@ -88,47 +86,6 @@ spec: number: 80 ``` -#### Traefik Ingress Controller - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: your-app-ingress - annotations: - traefik.ingress.kubernetes.io/router.middlewares: default-passkey-auth@kubernetescrd -spec: - rules: - - host: your-app.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: your-app-service - port: - number: 80 ---- -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: passkey-auth -spec: - forwardAuth: - address: https://your-passkey-auth.com/auth - authRequestHeaders: - - "X-Forwarded-Method" - - "X-Forwarded-Proto" - - "X-Forwarded-Host" - - "X-Forwarded-Uri" - - "X-Forwarded-For" - authResponseHeaders: - - "X-Auth-User" - - "X-Auth-Email" - authResponseHeadersRegex: "^X-" -``` - ## πŸ‘₯ User Management @@ -161,21 +118,6 @@ go run main.go # Access at http://localhost:8080 ``` -### Auth Endpoint Behavior - -The `/auth` endpoint automatically adapts to work with both Nginx and Traefik ingress controllers: - -**For Nginx auth_request:** -- Authenticated users: Returns `200 OK` with user headers -- Unauthenticated users: Returns `401 Unauthorized` - -**For Traefik ForwardAuth:** -- Authenticated users: Returns `200 OK` with user headers -- Unauthenticated users (with redirect param): Returns `302 Found` with `Location` header pointing to login page -- Unauthenticated users (without redirect param): Returns `401 Unauthorized` (fallback for Nginx) - -The endpoint detects the ingress controller type by checking for query parameters like `rd` or `redirect` that Traefik typically includes. - ### Key API Endpoints | Endpoint | Method | Description | @@ -184,7 +126,7 @@ The endpoint detects the ingress controller type by checking for query parameter | `/api/register/finish` | POST | Complete passkey registration | | `/api/login/begin` | POST | Start passkey authentication | | `/api/login/finish` | POST | Complete passkey authentication | -| `/auth` | GET | Auth check endpoint for ingress controllers | +| `/auth` | GET | Nginx auth check endpoint | | `/api/users` | GET/POST | List/create users | | `/health` | GET | Health check | diff --git a/helm/passkey-auth/Chart.yaml b/helm/passkey-auth/Chart.yaml index 874f35d..f5611b4 100644 --- a/helm/passkey-auth/Chart.yaml +++ b/helm/passkey-auth/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: passkey-auth -description: A WebAuthn-based passkey authentication provider for Kubernetes ingress controllers (Nginx & Traefik) +description: A WebAuthn-based passkey authentication provider for Kubernetes Nginx Ingress type: application -version: 0.1.2 +version: 0.1.1 appVersion: "main" home: https://github.com/wahyd4/passkey-auth sources: @@ -15,7 +15,6 @@ keywords: - webauthn - passkey - nginx-ingress - - traefik-ingress - security annotations: category: Security diff --git a/helm/passkey-auth/README.md b/helm/passkey-auth/README.md index 065e845..844b157 100644 --- a/helm/passkey-auth/README.md +++ b/helm/passkey-auth/README.md @@ -1,6 +1,6 @@ # Passkey Auth Helm Chart -A Helm chart for deploying Passkey Auth, a WebAuthn-based passkey authentication provider that integrates with Kubernetes ingress controllers (Nginx Ingress Controller and Traefik Ingress Controller). +A Helm chart for deploying Passkey Auth, a WebAuthn-based passkey authentication provider that integrates with Kubernetes Nginx Ingress controller. ## Overview @@ -18,7 +18,7 @@ helm upgrade --install my-passkey-auth -n home-apps -f my-values.yaml passkey-a - Kubernetes 1.19+ - Helm 3.0+ -- Ingress Controller (Nginx Ingress Controller or Traefik Ingress Controller) +- Nginx Ingress Controller - StorageClass for persistent volumes ## Installation @@ -85,8 +85,6 @@ ingress: ## Setup Authentication for Your Services -### Nginx Ingress Controller - Add these annotations to your ingress resources to protect them with passkey authentication: ```yaml @@ -102,40 +100,6 @@ spec: # ... your ingress spec ``` -### Traefik Ingress Controller - -For Traefik, create a ForwardAuth middleware and reference it in your ingress: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: passkey-auth - namespace: your-app-namespace -spec: - forwardAuth: - address: https://auth.example.com/auth - authRequestHeaders: - - "X-Forwarded-Method" - - "X-Forwarded-Proto" - - "X-Forwarded-Host" - - "X-Forwarded-Uri" - - "X-Forwarded-For" - authResponseHeaders: - - "X-Auth-User" - - "X-Auth-Email" - authResponseHeadersRegex: "^X-" ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: my-protected-app - annotations: - traefik.ingress.kubernetes.io/router.middlewares: your-app-namespace-passkey-auth@kubernetescrd -spec: - # ... your ingress spec -``` - ## Advanced Configuration ### Custom Storage @@ -292,11 +256,3 @@ secrets: | `nodeSelector` | Node labels for pod assignment | `{}` | | `tolerations` | Tolerations for pod assignment | `[]` | | `affinity` | Affinity for pod assignment | `{}` | - -### Traefik configuration - -| Name | Description | Value | -| ------------------------------ | --------------------------------------------------------------- | ------- | -| `traefik.enabled` | Enable Traefik-specific features | `false` | -| `traefik.middleware.create` | Create Traefik ForwardAuth middleware | `true` | -| `traefik.middleware.name` | Custom name for middleware (defaults to chart fullname) | `""` | diff --git a/helm/passkey-auth/examples/values-traefik.yaml b/helm/passkey-auth/examples/values-traefik.yaml deleted file mode 100644 index 2aed9ae..0000000 --- a/helm/passkey-auth/examples/values-traefik.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# Example values for Traefik deployment -# Copy this file and modify for your environment - -config: - webauthn: - rpId: "auth.example.com" - rpOrigins: - - "https://auth.example.com" - - cors: - allowedOrigins: - - "https://*.example.com" - - auth: - cookieDomain: ".example.com" - allowedEmails: - - "admin@example.com" - -secrets: - sessionSecret: "your-secure-random-secret-min-32-chars" - -ingress: - enabled: true - className: "traefik" - annotations: - kubernetes.io/tls-acme: "true" - cert-manager.io/cluster-issuer: "letsencrypt-prod" - hosts: - - host: auth.example.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: passkey-auth-tls - hosts: - - auth.example.com - -# Enable Traefik middleware creation -traefik: - enabled: true - middleware: - create: true - name: "passkey-auth" # Optional: custom name - -persistence: - enabled: true - size: 2Gi - -resources: - limits: - cpu: 400m - memory: 512Mi - requests: - cpu: 100m - memory: 128Mi diff --git a/helm/passkey-auth/templates/traefik-middleware.yaml b/helm/passkey-auth/templates/traefik-middleware.yaml deleted file mode 100644 index 25debc0..0000000 --- a/helm/passkey-auth/templates/traefik-middleware.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{{- if and .Values.traefik.enabled .Values.traefik.middleware.create }} -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: {{ default (include "passkey-auth.fullname" .) .Values.traefik.middleware.name }}-forwardauth - namespace: {{ .Release.Namespace }} - labels: - {{- include "passkey-auth.labels" . | nindent 4 }} -spec: - forwardAuth: - address: http{{ if .Values.ingress.tls }}s{{ end }}://{{ (index .Values.ingress.hosts 0).host }}/auth - authRequestHeaders: - - X-Forwarded-Method - - X-Forwarded-Proto - - X-Forwarded-Host - - X-Forwarded-Uri - - X-Forwarded-For - - Cookie - - Authorization - authResponseHeaders: - - X-Auth-User - - X-Auth-Email - authResponseHeadersRegex: "^X-" - authResponseHeaders: - - Location - authResponseHeadersRegex: ^X-|^Location$ -{{- end }} diff --git a/helm/passkey-auth/values.yaml b/helm/passkey-auth/values.yaml index 776f7ce..06bb682 100644 --- a/helm/passkey-auth/values.yaml +++ b/helm/passkey-auth/values.yaml @@ -166,10 +166,3 @@ volumeMounts: [] # Additional volumes volumes: [] - -# Traefik-specific configuration -traefik: - enabled: false - middleware: - create: true - name: "" # If empty, will use chart fullname diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 808208a..a6fe562 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -392,18 +392,17 @@ func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) { h.writeJSON(w, map[string]string{"status": "success"}) } -// AuthCheck implements auth backend for both nginx auth_request and Traefik ForwardAuth +// AuthCheck implements the nginx auth_request protocol func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) { // Debug logging logrus.Debugf("AuthCheck request from %s", r.RemoteAddr) logrus.Debugf("AuthCheck headers: %+v", r.Header) logrus.Debugf("AuthCheck cookies: %+v", r.Cookies()) - logrus.Debugf("AuthCheck query params: %+v", r.URL.Query()) session, err := h.store.Get(r, "auth-session") if err != nil { logrus.Errorf("Failed to get auth session: %v", err) - h.handleUnauthenticated(w, r) + w.WriteHeader(http.StatusUnauthorized) return } @@ -412,8 +411,8 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) { logrus.Debugf("Session values: %+v", session.Values) if !ok || !authenticated { - logrus.Debugf("User not authenticated") - h.handleUnauthenticated(w, r) + logrus.Debugf("User not authenticated, returning 401") + w.WriteHeader(http.StatusUnauthorized) return } @@ -429,41 +428,6 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// handleUnauthenticated handles unauthenticated requests for both Nginx and Traefik -func (h *Handlers) handleUnauthenticated(w http.ResponseWriter, r *http.Request) { - // Check for redirect parameter (Traefik ForwardAuth typically includes this) - redirectURL := r.URL.Query().Get("rd") - if redirectURL == "" { - // Also check for other common redirect parameter names - redirectURL = r.URL.Query().Get("redirect") - } - - // If redirect parameter is present, return 302 redirect (Traefik ForwardAuth) - if redirectURL != "" { - logrus.Debugf("Redirect parameter found (%s), returning 302 redirect for Traefik", redirectURL) - - // Construct login URL with redirect parameter - loginURL := "/login.html?redirect=" + redirectURL - - // If we have a host header, construct a full URL - if host := r.Header.Get("Host"); host != "" { - scheme := "http" - if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil { - scheme = "https" - } - loginURL = scheme + "://" + host + loginURL - } - - w.Header().Set("Location", loginURL) - w.WriteHeader(http.StatusFound) // 302 - return - } - - // No redirect parameter, return 401 (Nginx auth_request) - logrus.Debugf("No redirect parameter, returning 401 for Nginx auth_request") - w.WriteHeader(http.StatusUnauthorized) -} - // GetAuthStatus returns the current authentication status func (h *Handlers) GetAuthStatus(w http.ResponseWriter, r *http.Request) { session, err := h.store.Get(r, "auth-session") diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go deleted file mode 100644 index 2894adf..0000000 --- a/internal/handlers/handlers_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package handlers - -import ( - "net/http" - "net/http/httptest" - "testing" - - "passkey-auth/internal/auth" - "passkey-auth/internal/config" - "passkey-auth/internal/database" -) - -func TestAuthCheck(t *testing.T) { - // Setup test config - cfg := &config.Config{ - Auth: config.AuthConfig{ - SessionSecret: "test-secret", - }, - WebAuthn: config.WebAuthnConfig{ - RPDisplayName: "Test Passkey Auth", - RPID: "localhost", - RPOrigins: []string{"http://localhost:8080"}, - }, - } - - // Setup test database - db, err := database.New(":memory:") - if err != nil { - t.Fatalf("Failed to create test database: %v", err) - } - defer db.Close() - - // Setup test WebAuthn (mock) - webAuthn, err := auth.NewWebAuthn(cfg) - if err != nil { - t.Fatalf("Failed to create test WebAuthn: %v", err) - } - - // Create handlers - h := New(db, webAuthn, cfg) - - tests := []struct { - name string - queryParams string - authenticated bool - expectedStatus int - expectedHeader string - }{ - { - name: "Unauthenticated without redirect param (Nginx)", - queryParams: "", - authenticated: false, - expectedStatus: http.StatusUnauthorized, - expectedHeader: "", - }, - { - name: "Unauthenticated with rd param (Traefik)", - queryParams: "rd=https://example.com/protected", - authenticated: false, - expectedStatus: http.StatusFound, - expectedHeader: "/login.html?redirect=https://example.com/protected", - }, - { - name: "Unauthenticated with redirect param (Traefik)", - queryParams: "redirect=https://example.com/protected", - authenticated: false, - expectedStatus: http.StatusFound, - expectedHeader: "/login.html?redirect=https://example.com/protected", - }, - { - name: "Authenticated with redirect param", - queryParams: "rd=https://example.com/protected", - authenticated: true, - expectedStatus: http.StatusOK, - expectedHeader: "", - }, - { - name: "Authenticated without redirect param", - queryParams: "", - authenticated: true, - expectedStatus: http.StatusOK, - expectedHeader: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create request - req, err := http.NewRequest("GET", "/auth?"+tt.queryParams, nil) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - - // Mock session if authenticated - if tt.authenticated { - // Create a session cookie for testing - session, _ := h.store.Get(req, "auth-session") - session.Values["authenticated"] = true - session.Values["user_id"] = 1 - session.Values["user_email"] = "test@example.com" - - // Create a response recorder to capture the session cookie - w := httptest.NewRecorder() - session.Save(req, w) - - // Extract the cookie and add it to the request - for _, cookie := range w.Result().Cookies() { - if cookie.Name == "auth-session" { - req.AddCookie(cookie) - break - } - } - } - - // Create response recorder - w := httptest.NewRecorder() - - // Call the handler - h.AuthCheck(w, req) - - // Check status code - if w.Code != tt.expectedStatus { - t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code) - } - - // Check Location header for redirects - if tt.expectedStatus == http.StatusFound { - location := w.Header().Get("Location") - if location != tt.expectedHeader { - t.Errorf("Expected Location header %s, got %s", tt.expectedHeader, location) - } - } - - // Check auth headers for authenticated requests - if tt.authenticated && tt.expectedStatus == http.StatusOK { - userHeader := w.Header().Get("X-Auth-User") - if userHeader == "" { - t.Error("Expected X-Auth-User header for authenticated request") - } - } - }) - } -} - -func TestAuthCheckWithHost(t *testing.T) { - // Setup test config - cfg := &config.Config{ - Auth: config.AuthConfig{ - SessionSecret: "test-secret", - }, - WebAuthn: config.WebAuthnConfig{ - RPDisplayName: "Test Passkey Auth", - RPID: "localhost", - RPOrigins: []string{"http://localhost:8080"}, - }, - } - - // Setup test database - db, err := database.New(":memory:") - if err != nil { - t.Fatalf("Failed to create test database: %v", err) - } - defer db.Close() - - // Setup test WebAuthn - webAuthn, err := auth.NewWebAuthn(cfg) - if err != nil { - t.Fatalf("Failed to create test WebAuthn: %v", err) - } - - // Create handlers - h := New(db, webAuthn, cfg) - - // Test with Host header - req, err := http.NewRequest("GET", "/auth?rd=https://example.com/protected", nil) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - req.Header.Set("Host", "auth.example.com") - req.Header.Set("X-Forwarded-Proto", "https") - - w := httptest.NewRecorder() - h.AuthCheck(w, req) - - // Should return 302 with full URL - if w.Code != http.StatusFound { - t.Errorf("Expected status %d, got %d", http.StatusFound, w.Code) - } - - expectedLocation := "https://auth.example.com/login.html?redirect=https://example.com/protected" - location := w.Header().Get("Location") - if location != expectedLocation { - t.Errorf("Expected Location header %s, got %s", expectedLocation, location) - } -} diff --git a/main.go b/main.go index 9cb263e..24ac014 100644 --- a/main.go +++ b/main.go @@ -61,9 +61,7 @@ func main() { api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT") api.HandleFunc("/users/{id}", h.DeleteUser).Methods("DELETE") - // Auth backend endpoint for ingress controllers (Nginx auth_request & Traefik ForwardAuth) - // - For Nginx: Returns 200 for authenticated, 401 for unauthenticated - // - For Traefik: Returns 200 for authenticated, 302 redirect for unauthenticated (when rd param present) + // Nginx auth backend endpoint router.HandleFunc("/auth", h.AuthCheck).Methods("GET", "HEAD") // Health check