From 20952aa5149b67f20152a4446593001fb35d98cc Mon Sep 17 00:00:00 2001 From: MorezMartin Date: Wed, 28 Jan 2026 02:27:02 +0100 Subject: [PATCH] Fix jwt error in admin UI (#8140) * add jwt token in weed admin headers requests * add jwt token to header for download * :s/upload/download * filer_signing.read despite of filer_signing key * finalize filer_browser_handlers.go * admin: add JWT authorization to file browser handlers * security: fix typos in JWT read validation descriptions * Move security.toml to example and secure keys * security: address PR feedback on JWT enforcement and example keys * security: refactor JWT logic and improve example keys readability * Update docker/Dockerfile.local Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Chris Lu Co-authored-by: Chris Lu Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 1 + docker/Dockerfile.local | 1 + docker/security.toml.example | 34 +++++++++++ .../templates/shared/security-configmap.yaml | 2 +- weed/admin/handlers/file_browser_handlers.go | 61 ++++++++++++++++--- weed/command/scaffold/security.toml | 2 +- 6 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 docker/security.toml.example diff --git a/.gitignore b/.gitignore index a4446b7de..0ea9a06b0 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,4 @@ test/s3/iam/.test_env /test/erasure_coding/admin_dockertest/tmp /test/erasure_coding/admin_dockertest/task_logs weed_bin +.aider* diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local index 9ea378401..125dc7c9b 100644 --- a/docker/Dockerfile.local +++ b/docker/Dockerfile.local @@ -4,6 +4,7 @@ COPY ./weed /usr/bin/weed RUN chmod +x /usr/bin/weed && ls -la /usr/bin/weed RUN mkdir -p /etc/seaweedfs COPY ./filer.toml /etc/seaweedfs/filer.toml +COPY ./security.toml.example /etc/seaweedfs/security.toml COPY ./entrypoint.sh /entrypoint.sh # Install dependencies and create non-root user diff --git a/docker/security.toml.example b/docker/security.toml.example new file mode 100644 index 000000000..2d1702759 --- /dev/null +++ b/docker/security.toml.example @@ -0,0 +1,34 @@ +# Put this file to one of the location, with descending priority +# ./security.toml +# $HOME/.seaweedfs/security.toml +# /etc/seaweedfs/security.toml +# this file is read by master, volume server, filer, and worker + +# comma separated origins allowed to make requests to the filer and s3 gateway. +# enter in this format: https://domain.com, or http://localhost:port +[cors.allowed_origins] +values = "*" + +# this jwt signing key is read by master and volume server, and it is used for write operations: +# - the Master server generates the JWT, which can be used to write a certain file on a volume server +# - the Volume server validates the JWT on writing +# the jwt defaults to expire after 10 seconds. +[jwt.signing] +key = "V1JJVEVTRUNSRVRFWEFNUExFMTIzNDU2Nzg5MDEy" # Example: WRITESECRETEXAMPLE123456789012 +# this jwt signing key is read by master and volume server, and it is used for read operations: +# - the Master server generates the JWT, which can be used to read a certain file on a volume server +# - the Volume server validates the JWT on reading +[jwt.signing.read] +key = "UkVBRFNFQ1JFVUVYQU1QTEUxMjM0NTY3ODkwMTI=" # Example: READSECRETEXAMPLE123456789012 +# If this JWT key is configured, Filer only accepts writes over HTTP if they are signed with this JWT: +# - f.e. the S3 API Shim generates the JWT +# - the Filer server validates the JWT on writing +# the jwt defaults to expire after 10 seconds. +[jwt.filer_signing] +key = "RklMRVJXUklURVNFQ1JFVEVYQU1QTEUxMjM0NTY3OA==" # Example: FILERWRITESECRETEXAMPLE12345678 +# If this JWT key is configured, Filer only accepts reads over HTTP if they are signed with this JWT: +# - f.e. the S3 API Shim generates the JWT +# - the Filer server validates the JWT on reading +# the jwt defaults to expire after 10 seconds. +[jwt.filer_signing.read] +key = "RklMRVJSRUFEU0VDUkVURVhBTVBMRTEyMzQ1Njc4OQ==" # Example: FILERREADSECRETEXAMPLE123456789 diff --git a/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml b/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml index d47ad2a4c..07e6c6dcc 100644 --- a/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml +++ b/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml @@ -48,7 +48,7 @@ data: {{- if .Values.global.securityConfig.jwtSigning.filerRead }} # If this JWT key is configured, Filer only accepts reads over HTTP if they are signed with this JWT: # - f.e. the S3 API Shim generates the JWT - # - the Filer server validates the JWT on writing + # - the Filer server validates the JWT on reading # the jwt defaults to expire after 10 seconds. [jwt.filer_signing.read] key = "{{ dig "jwt" "filer_signing" "read" "key" (randAlphaNum 10 | b64enc) $securityConfig }}" diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index b7c44a69d..8cfd304df 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -22,6 +22,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/security" "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/util/http/client" ) @@ -121,7 +122,6 @@ func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) { }) return err }) - if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()}) return @@ -228,7 +228,7 @@ func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) { Name: filepath.Base(fullPath), IsDirectory: true, Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode + FileMode: uint32(0o755 | os.ModeDir), // Directory mode Uid: filer_pb.OS_UID, Gid: filer_pb.OS_GID, Crtime: time.Now().Unix(), @@ -239,7 +239,6 @@ func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) { }) return err }) - if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder: " + err.Error()}) return @@ -407,6 +406,9 @@ func (h *FileBrowserHandlers) uploadFileToFiler(filePath string, fileHeader *mul // Set content type with boundary req.Header.Set("Content-Type", writer.FormDataContentType()) + // Add JWT Token to Authorization Header + h.setupFilerJwtAuth(req, "jwt.filer_signing.key", "jwt.filer_signing.expires_after_seconds", "filer upload") + // Send request using TLS-aware HTTP client with 60s timeout for large file uploads // lgtm[go/ssrf] // Safe: filerAddress validated by validateFilerAddress() to match configured filer @@ -525,7 +527,12 @@ func (h *FileBrowserHandlers) fetchFileContent(filePath string, timeout time.Dur // Safe: filerAddress validated by validateFilerAddress() to match configured filer // Safe: cleanFilePath validated and cleaned by validateAndCleanFilePath() to prevent path traversal client := h.newClientWithTimeout(timeout) - resp, err := client.Get(fileURL) + req, err := http.NewRequest("GET", fileURL, nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + h.addFilerJwtAuthHeader(req) + resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("failed to fetch file from filer: %w", err) } @@ -595,6 +602,9 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) { return } client := h.newClientWithTimeout(5 * time.Minute) // Longer timeout for large file downloads + + h.addFilerJwtAuthHeader(req) + resp, err := client.Do(req) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to fetch file from filer: " + err.Error()}) @@ -687,7 +697,6 @@ func (h *FileBrowserHandlers) ViewFile(c *gin.Context) { return nil }) - if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file metadata: " + err.Error()}) return @@ -837,7 +846,6 @@ func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) { return nil }) - if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file properties: " + err.Error()}) return @@ -1032,7 +1040,13 @@ func (h *FileBrowserHandlers) isLikelyTextFile(filePath string, maxCheckSize int // Safe: filerAddress validated by validateFilerAddress() to match configured filer // Safe: cleanFilePath validated and cleaned by validateAndCleanFilePath() to prevent path traversal client := h.newClientWithTimeout(10 * time.Second) - resp, err := client.Get(fileURL) + req, err := http.NewRequest("GET", fileURL, nil) + if err != nil { + glog.Errorf("Failed to create request: %v", err) + return false + } + h.addFilerJwtAuthHeader(req) + resp, err := client.Do(req) if err != nil { return false } @@ -1086,3 +1100,36 @@ func min(a, b int64) int64 { } return b } + +// setupFilerJwtAuth generates a JWT token and adds it to the request Authorization header if configured. +func (h *FileBrowserHandlers) setupFilerJwtAuth(req *http.Request, keyPath, expiresPath, operation string) { + // Load security configuration + v := util.GetViper() + + // Read Filer JWT token from security.toml + signingKey := security.SigningKey(v.GetString(keyPath)) + expiresAfterSec := v.GetInt(expiresPath) + + // Generate JWT token to authenticate with Filer + var jwtToken security.EncodedJwt + if len(signingKey) > 0 { + jwtToken = security.GenJwtForFilerServer(signingKey, expiresAfterSec) + glog.V(4).Infof("Generated JWT token for %s (expires in %d sec)", operation, expiresAfterSec) + } else { + if v.GetString("jwt.signing.key") != "" { + glog.Warningf("JWT %s key not configured, but general JWT security is enabled. %s without authentication.", keyPath, operation) + } else { + glog.V(1).Infof("No JWT signing key configured, %s without authentication", operation) + } + } + + // Add JWT Token to Authorization Header + if jwtToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(jwtToken))) + glog.V(4).Infof("Added JWT authorization header for %s", operation) + } +} + +func (h *FileBrowserHandlers) addFilerJwtAuthHeader(req *http.Request) { + h.setupFilerJwtAuth(req, "jwt.filer_signing.read.key", "jwt.filer_signing.read.expires_after_seconds", "filer request") +} diff --git a/weed/command/scaffold/security.toml b/weed/command/scaffold/security.toml index f18df202c..19ed5337d 100644 --- a/weed/command/scaffold/security.toml +++ b/weed/command/scaffold/security.toml @@ -50,7 +50,7 @@ expires_after_seconds = 10 # seconds # If this JWT key is configured, Filer only accepts reads over HTTP if they are signed with this JWT: # - f.e. the S3 API Shim generates the JWT -# - the Filer server validates the JWT on writing +# - the Filer server validates the JWT on reading # the jwt defaults to expire after 10 seconds. [jwt.filer_signing.read] key = ""