You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
12 KiB

5 years ago
5 years ago
FEATURE: add JWT to HTTP endpoints of Filer and use them in S3 Client - one JWT for reading and one for writing, analogous to how the JWT between Master and Volume Server works - I did not implement IP `whiteList` parameter on the filer Additionally, because http_util.DownloadFile now sets the JWT, the `download` command should now work when `jwt.signing.read` is configured. By looking at the code, I think this case did not work before. ## Docs to be adjusted after a release Page `Amazon-S3-API`: ``` # Authentication with Filer You can use mTLS for the gRPC connection between S3-API-Proxy and the filer, as explained in [Security-Configuration](Security-Configuration) - controlled by the `grpc.*` configuration in `security.toml`. Starting with version XX, it is also possible to authenticate the HTTP operations between the S3-API-Proxy and the Filer (especially uploading new files). This is configured by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. With both configurations (gRPC and JWT), it is possible to have Filer and S3 communicate in fully authenticated fashion; so Filer will reject any unauthenticated communication. ``` Page `Security Overview`: ``` The following items are not covered, yet: - master server http REST services Starting with version XX, the Filer HTTP REST services can be secured with a JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. ... Before version XX: "weed filer -disableHttp", disable http operations, only gRPC operations are allowed. This works with "weed mount" by FUSE. It does **not work** with the [S3 Gateway](Amazon S3 API), as this does HTTP calls to the Filer. Starting with version XX: secured by JWT, by setting `filer_jwt.signing.key` and `filer_jwt.signing.read.key` in `security.toml`. **This now works with the [S3 Gateway](Amazon S3 API).** ... # Securing Filer HTTP with JWT To enable JWT-based access control for the Filer, 1. generate `security.toml` file by `weed scaffold -config=security` 2. set `filer_jwt.signing.key` to a secret string - and optionally filer_jwt.signing.read.key` as well to a secret string 3. copy the same `security.toml` file to the filers and all S3 proxies. If `filer_jwt.signing.key` is configured: When sending upload/update/delete HTTP operations to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.key`. If `filer_jwt.signing.read.key` is configured: When sending GET or HEAD requests to a filer server, the request header `Authorization` should be the JWT string (`Authorization: Bearer [JwtToken]`). The operation is authorized after the filer validates the JWT with `filer_jwt.signing.read.key`. The S3 API Gateway reads the above JWT keys and sends authenticated HTTP requests to the filer. ``` Page `Security Configuration`: ``` (update scaffold file) ... [filer_jwt.signing] key = "blahblahblahblah" [filer_jwt.signing.read] key = "blahblahblahblah" ``` Resolves: #158
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
  1. package s3api
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/seaweedfs/seaweedfs/weed/filer"
  6. "github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
  7. "github.com/seaweedfs/seaweedfs/weed/s3api/s3account"
  8. "net"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "github.com/gorilla/mux"
  13. "github.com/seaweedfs/seaweedfs/weed/pb"
  14. . "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  15. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  16. "github.com/seaweedfs/seaweedfs/weed/security"
  17. "github.com/seaweedfs/seaweedfs/weed/util"
  18. "google.golang.org/grpc"
  19. )
  20. type S3ApiServerOption struct {
  21. Filer pb.ServerAddress
  22. Port int
  23. Config string
  24. DomainName string
  25. BucketsPath string
  26. GrpcDialOption grpc.DialOption
  27. AllowEmptyFolder bool
  28. AllowDeleteBucketNotEmpty bool
  29. LocalFilerSocket string
  30. DataCenter string
  31. FilerGroup string
  32. }
  33. type S3ApiServer struct {
  34. s3_pb.UnimplementedSeaweedS3Server
  35. option *S3ApiServerOption
  36. iam *IdentityAccessManagement
  37. cb *CircuitBreaker
  38. randomClientId int32
  39. filerGuard *security.Guard
  40. client *http.Client
  41. accountManager *s3account.AccountManager
  42. bucketRegistry *BucketRegistry
  43. }
  44. func NewS3ApiServer(router *mux.Router, option *S3ApiServerOption) (s3ApiServer *S3ApiServer, err error) {
  45. v := util.GetViper()
  46. signingKey := v.GetString("jwt.filer_signing.key")
  47. v.SetDefault("jwt.filer_signing.expires_after_seconds", 10)
  48. expiresAfterSec := v.GetInt("jwt.filer_signing.expires_after_seconds")
  49. readSigningKey := v.GetString("jwt.filer_signing.read.key")
  50. v.SetDefault("jwt.filer_signing.read.expires_after_seconds", 60)
  51. readExpiresAfterSec := v.GetInt("jwt.filer_signing.read.expires_after_seconds")
  52. s3ApiServer = &S3ApiServer{
  53. option: option,
  54. iam: NewIdentityAccessManagement(option),
  55. randomClientId: util.RandomInt32(),
  56. filerGuard: security.NewGuard([]string{}, signingKey, expiresAfterSec, readSigningKey, readExpiresAfterSec),
  57. cb: NewCircuitBreaker(option),
  58. }
  59. s3ApiServer.accountManager = s3account.NewAccountManager(s3ApiServer)
  60. s3ApiServer.bucketRegistry = NewBucketRegistry(s3ApiServer)
  61. if option.LocalFilerSocket == "" {
  62. s3ApiServer.client = &http.Client{Transport: &http.Transport{
  63. MaxIdleConns: 1024,
  64. MaxIdleConnsPerHost: 1024,
  65. }}
  66. } else {
  67. s3ApiServer.client = &http.Client{
  68. Transport: &http.Transport{
  69. DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
  70. return net.Dial("unix", option.LocalFilerSocket)
  71. },
  72. },
  73. }
  74. }
  75. s3ApiServer.registerRouter(router)
  76. go s3ApiServer.subscribeMetaEvents("s3", time.Now().UnixNano(), filer.DirectoryEtcRoot, []string{option.BucketsPath})
  77. return s3ApiServer, nil
  78. }
  79. func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
  80. // API Router
  81. apiRouter := router.PathPrefix("/").Subrouter()
  82. // Readiness Probe
  83. apiRouter.Methods("GET").Path("/status").HandlerFunc(s3a.StatusHandler)
  84. apiRouter.Methods("OPTIONS").HandlerFunc(
  85. func(w http.ResponseWriter, r *http.Request) {
  86. w.Header().Set("Access-Control-Allow-Origin", "*")
  87. w.Header().Set("Access-Control-Expose-Headers", "*")
  88. w.Header().Set("Access-Control-Allow-Methods", "*")
  89. w.Header().Set("Access-Control-Allow-Headers", "*")
  90. writeSuccessResponseEmpty(w, r)
  91. })
  92. var routers []*mux.Router
  93. if s3a.option.DomainName != "" {
  94. domainNames := strings.Split(s3a.option.DomainName, ",")
  95. for _, domainName := range domainNames {
  96. routers = append(routers, apiRouter.Host(
  97. fmt.Sprintf("%s.%s:%d", "{bucket:.+}", domainName, s3a.option.Port)).Subrouter())
  98. routers = append(routers, apiRouter.Host(
  99. fmt.Sprintf("%s.%s", "{bucket:.+}", domainName)).Subrouter())
  100. }
  101. }
  102. routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
  103. for _, bucket := range routers {
  104. // each case should follow the next rule:
  105. // - requesting object with query must precede any other methods
  106. // - requesting object must precede any methods with buckets
  107. // - requesting bucket with query must precede raw methods with buckets
  108. // - requesting bucket must be processed in the end
  109. // objects with query
  110. // CopyObjectPart
  111. bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", `.*?(\/|%2F).*?`).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CopyObjectPartHandler, ACTION_WRITE)), "PUT")).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
  112. // PutObjectPart
  113. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectPartHandler, ACTION_WRITE)), "PUT")).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
  114. // CompleteMultipartUpload
  115. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CompleteMultipartUploadHandler, ACTION_WRITE)), "POST")).Queries("uploadId", "{uploadId:.*}")
  116. // NewMultipartUpload
  117. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.NewMultipartUploadHandler, ACTION_WRITE)), "POST")).Queries("uploads", "")
  118. // AbortMultipartUpload
  119. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.AbortMultipartUploadHandler, ACTION_WRITE)), "DELETE")).Queries("uploadId", "{uploadId:.*}")
  120. // ListObjectParts
  121. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectPartsHandler, ACTION_READ)), "GET")).Queries("uploadId", "{uploadId:.*}")
  122. // ListMultipartUploads
  123. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListMultipartUploadsHandler, ACTION_READ)), "GET")).Queries("uploads", "")
  124. // GetObjectTagging
  125. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectTaggingHandler, ACTION_READ)), "GET")).Queries("tagging", "")
  126. // PutObjectTagging
  127. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectTaggingHandler, ACTION_TAGGING)), "PUT")).Queries("tagging", "")
  128. // DeleteObjectTagging
  129. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteObjectTaggingHandler, ACTION_TAGGING)), "DELETE")).Queries("tagging", "")
  130. // PutObjectACL
  131. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectAclHandler, ACTION_WRITE_ACP)), "PUT")).Queries("acl", "")
  132. // PutObjectRetention
  133. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectRetentionHandler, ACTION_WRITE)), "PUT")).Queries("retention", "")
  134. // PutObjectLegalHold
  135. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLegalHoldHandler, ACTION_WRITE)), "PUT")).Queries("legal-hold", "")
  136. // PutObjectLockConfiguration
  137. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
  138. // GetObjectACL
  139. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectAclHandler, ACTION_READ_ACP)), "GET")).Queries("acl", "")
  140. // objects with query
  141. // raw objects
  142. // HeadObject
  143. bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadObjectHandler, ACTION_READ)), "GET"))
  144. // GetObject, but directory listing is not supported
  145. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectHandler, ACTION_READ)), "GET"))
  146. // CopyObject
  147. bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CopyObjectHandler, ACTION_WRITE)), "COPY"))
  148. // PutObject
  149. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectHandler, ACTION_WRITE)), "PUT"))
  150. // DeleteObject
  151. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteObjectHandler, ACTION_WRITE)), "DELETE"))
  152. // raw objects
  153. // buckets with query
  154. // DeleteMultipleObjects
  155. bucket.Methods("POST").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteMultipleObjectsHandler, ACTION_WRITE)), "DELETE")).Queries("delete", "")
  156. // GetBucketACL
  157. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketAclHandler, ACTION_READ_ACP)), "GET")).Queries("acl", "")
  158. // PutBucketACL
  159. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketAclHandler, ACTION_WRITE_ACP)), "PUT")).Queries("acl", "")
  160. // GetBucketPolicy
  161. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketPolicyHandler, ACTION_READ)), "GET")).Queries("policy", "")
  162. // PutBucketPolicy
  163. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketPolicyHandler, ACTION_WRITE)), "PUT")).Queries("policy", "")
  164. // DeleteBucketPolicy
  165. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketPolicyHandler, ACTION_WRITE)), "DELETE")).Queries("policy", "")
  166. // GetBucketCors
  167. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketCorsHandler, ACTION_READ)), "GET")).Queries("cors", "")
  168. // PutBucketCors
  169. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketCorsHandler, ACTION_WRITE)), "PUT")).Queries("cors", "")
  170. // DeleteBucketCors
  171. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketCorsHandler, ACTION_WRITE)), "DELETE")).Queries("cors", "")
  172. // GetBucketLifecycleConfiguration
  173. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketLifecycleConfigurationHandler, ACTION_READ)), "GET")).Queries("lifecycle", "")
  174. // PutBucketLifecycleConfiguration
  175. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketLifecycleConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("lifecycle", "")
  176. // DeleteBucketLifecycleConfiguration
  177. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketLifecycleHandler, ACTION_WRITE)), "DELETE")).Queries("lifecycle", "")
  178. // GetBucketLocation
  179. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketLocationHandler, ACTION_READ)), "GET")).Queries("location", "")
  180. // GetBucketRequestPayment
  181. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketRequestPaymentHandler, ACTION_READ)), "GET")).Queries("requestPayment", "")
  182. // ListObjectsV2
  183. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV2Handler, ACTION_LIST)), "LIST")).Queries("list-type", "2")
  184. // buckets with query
  185. // PutBucketOwnershipControls
  186. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.PutBucketOwnershipControls, ACTION_ADMIN), "PUT")).Queries("ownershipControls", "")
  187. //GetBucketOwnershipControls
  188. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.GetBucketOwnershipControls, ACTION_READ), "GET")).Queries("ownershipControls", "")
  189. //DeleteBucketOwnershipControls
  190. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.DeleteBucketOwnershipControls, ACTION_ADMIN), "DELETE")).Queries("ownershipControls", "")
  191. // raw buckets
  192. // PostPolicy
  193. bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST"))
  194. // HeadBucket
  195. bucket.Methods("HEAD").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadBucketHandler, ACTION_READ)), "GET"))
  196. // PutBucket
  197. bucket.Methods("PUT").HandlerFunc(track(s3a.PutBucketHandler, "PUT"))
  198. // DeleteBucket
  199. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketHandler, ACTION_WRITE)), "DELETE"))
  200. // ListObjectsV1 (Legacy)
  201. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV1Handler, ACTION_LIST)), "LIST"))
  202. // raw buckets
  203. }
  204. // ListBuckets
  205. apiRouter.Methods("GET").Path("/").HandlerFunc(track(s3a.ListBucketsHandler, "LIST"))
  206. // NotFound
  207. apiRouter.NotFoundHandler = http.HandlerFunc(s3err.NotFoundHandler)
  208. }