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.

268 lines
13 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
1 year 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/glog"
  7. "github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
  8. "github.com/seaweedfs/seaweedfs/weed/util/grace"
  9. "net"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "github.com/gorilla/mux"
  14. "github.com/seaweedfs/seaweedfs/weed/pb"
  15. . "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  16. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  17. "github.com/seaweedfs/seaweedfs/weed/security"
  18. "github.com/seaweedfs/seaweedfs/weed/util"
  19. "google.golang.org/grpc"
  20. )
  21. type S3ApiServerOption struct {
  22. Filer pb.ServerAddress
  23. Port int
  24. Config string
  25. DomainName string
  26. BucketsPath string
  27. GrpcDialOption grpc.DialOption
  28. AllowEmptyFolder bool
  29. AllowDeleteBucketNotEmpty bool
  30. LocalFilerSocket string
  31. DataCenter string
  32. FilerGroup string
  33. }
  34. type S3ApiServer struct {
  35. s3_pb.UnimplementedSeaweedS3Server
  36. option *S3ApiServerOption
  37. iam *IdentityAccessManagement
  38. cb *CircuitBreaker
  39. randomClientId int32
  40. filerGuard *security.Guard
  41. client *http.Client
  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. if option.Config != "" {
  60. grace.OnReload(func() {
  61. if err := s3ApiServer.iam.loadS3ApiConfigurationFromFile(option.Config); err != nil {
  62. glog.Errorf("fail to load config file %s: %v", option.Config, err)
  63. } else {
  64. glog.V(0).Infof("Loaded %d identities from config file %s", len(s3ApiServer.iam.identities), option.Config)
  65. }
  66. })
  67. }
  68. s3ApiServer.bucketRegistry = NewBucketRegistry(s3ApiServer)
  69. if option.LocalFilerSocket == "" {
  70. s3ApiServer.client = &http.Client{Transport: &http.Transport{
  71. MaxIdleConns: 1024,
  72. MaxIdleConnsPerHost: 1024,
  73. }}
  74. } else {
  75. s3ApiServer.client = &http.Client{
  76. Transport: &http.Transport{
  77. DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
  78. return net.Dial("unix", option.LocalFilerSocket)
  79. },
  80. },
  81. }
  82. }
  83. s3ApiServer.registerRouter(router)
  84. go s3ApiServer.subscribeMetaEvents("s3", time.Now().UnixNano(), filer.DirectoryEtcRoot, []string{option.BucketsPath})
  85. return s3ApiServer, nil
  86. }
  87. func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
  88. // API Router
  89. apiRouter := router.PathPrefix("/").Subrouter()
  90. // Readiness Probe
  91. apiRouter.Methods("GET").Path("/status").HandlerFunc(s3a.StatusHandler)
  92. apiRouter.Methods("OPTIONS").HandlerFunc(
  93. func(w http.ResponseWriter, r *http.Request) {
  94. w.Header().Set("Access-Control-Allow-Origin", "*")
  95. w.Header().Set("Access-Control-Expose-Headers", "*")
  96. w.Header().Set("Access-Control-Allow-Methods", "*")
  97. w.Header().Set("Access-Control-Allow-Headers", "*")
  98. writeSuccessResponseEmpty(w, r)
  99. })
  100. var routers []*mux.Router
  101. if s3a.option.DomainName != "" {
  102. domainNames := strings.Split(s3a.option.DomainName, ",")
  103. for _, domainName := range domainNames {
  104. routers = append(routers, apiRouter.Host(
  105. fmt.Sprintf("%s.%s:%d", "{bucket:.+}", domainName, s3a.option.Port)).Subrouter())
  106. routers = append(routers, apiRouter.Host(
  107. fmt.Sprintf("%s.%s", "{bucket:.+}", domainName)).Subrouter())
  108. }
  109. }
  110. routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
  111. for _, bucket := range routers {
  112. // each case should follow the next rule:
  113. // - requesting object with query must precede any other methods
  114. // - requesting object must precede any methods with buckets
  115. // - requesting bucket with query must precede raw methods with buckets
  116. // - requesting bucket must be processed in the end
  117. // objects with query
  118. // CopyObjectPart
  119. 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:.*}")
  120. // PutObjectPart
  121. 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:.*}")
  122. // CompleteMultipartUpload
  123. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CompleteMultipartUploadHandler, ACTION_WRITE)), "POST")).Queries("uploadId", "{uploadId:.*}")
  124. // NewMultipartUpload
  125. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.NewMultipartUploadHandler, ACTION_WRITE)), "POST")).Queries("uploads", "")
  126. // AbortMultipartUpload
  127. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.AbortMultipartUploadHandler, ACTION_WRITE)), "DELETE")).Queries("uploadId", "{uploadId:.*}")
  128. // ListObjectParts
  129. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectPartsHandler, ACTION_READ)), "GET")).Queries("uploadId", "{uploadId:.*}")
  130. // ListMultipartUploads
  131. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListMultipartUploadsHandler, ACTION_READ)), "GET")).Queries("uploads", "")
  132. // GetObjectTagging
  133. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectTaggingHandler, ACTION_READ)), "GET")).Queries("tagging", "")
  134. // PutObjectTagging
  135. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectTaggingHandler, ACTION_TAGGING)), "PUT")).Queries("tagging", "")
  136. // DeleteObjectTagging
  137. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteObjectTaggingHandler, ACTION_TAGGING)), "DELETE")).Queries("tagging", "")
  138. // PutObjectACL
  139. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectAclHandler, ACTION_WRITE_ACP)), "PUT")).Queries("acl", "")
  140. // PutObjectRetention
  141. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectRetentionHandler, ACTION_WRITE)), "PUT")).Queries("retention", "")
  142. // PutObjectLegalHold
  143. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLegalHoldHandler, ACTION_WRITE)), "PUT")).Queries("legal-hold", "")
  144. // PutObjectLockConfiguration
  145. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectLockConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("object-lock", "")
  146. // GetObjectACL
  147. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectAclHandler, ACTION_READ_ACP)), "GET")).Queries("acl", "")
  148. // objects with query
  149. // raw objects
  150. // HeadObject
  151. bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadObjectHandler, ACTION_READ)), "GET"))
  152. // GetObject, but directory listing is not supported
  153. bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectHandler, ACTION_READ)), "GET"))
  154. // CopyObject
  155. bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CopyObjectHandler, ACTION_WRITE)), "COPY"))
  156. // PutObject
  157. bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutObjectHandler, ACTION_WRITE)), "PUT"))
  158. // DeleteObject
  159. bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteObjectHandler, ACTION_WRITE)), "DELETE"))
  160. // raw objects
  161. // buckets with query
  162. // DeleteMultipleObjects
  163. bucket.Methods("POST").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteMultipleObjectsHandler, ACTION_WRITE)), "DELETE")).Queries("delete", "")
  164. // GetBucketACL
  165. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketAclHandler, ACTION_READ_ACP)), "GET")).Queries("acl", "")
  166. // PutBucketACL
  167. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketAclHandler, ACTION_WRITE_ACP)), "PUT")).Queries("acl", "")
  168. // GetBucketPolicy
  169. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketPolicyHandler, ACTION_READ)), "GET")).Queries("policy", "")
  170. // PutBucketPolicy
  171. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketPolicyHandler, ACTION_WRITE)), "PUT")).Queries("policy", "")
  172. // DeleteBucketPolicy
  173. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketPolicyHandler, ACTION_WRITE)), "DELETE")).Queries("policy", "")
  174. // GetBucketCors
  175. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketCorsHandler, ACTION_READ)), "GET")).Queries("cors", "")
  176. // PutBucketCors
  177. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketCorsHandler, ACTION_WRITE)), "PUT")).Queries("cors", "")
  178. // DeleteBucketCors
  179. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketCorsHandler, ACTION_WRITE)), "DELETE")).Queries("cors", "")
  180. // GetBucketLifecycleConfiguration
  181. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketLifecycleConfigurationHandler, ACTION_READ)), "GET")).Queries("lifecycle", "")
  182. // PutBucketLifecycleConfiguration
  183. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketLifecycleConfigurationHandler, ACTION_WRITE)), "PUT")).Queries("lifecycle", "")
  184. // DeleteBucketLifecycleConfiguration
  185. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketLifecycleHandler, ACTION_WRITE)), "DELETE")).Queries("lifecycle", "")
  186. // GetBucketLocation
  187. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketLocationHandler, ACTION_READ)), "GET")).Queries("location", "")
  188. // GetBucketRequestPayment
  189. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketRequestPaymentHandler, ACTION_READ)), "GET")).Queries("requestPayment", "")
  190. // GetBucketVersioning
  191. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetBucketVersioningHandler, ACTION_READ)), "GET")).Queries("versioning", "")
  192. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketVersioningHandler, ACTION_WRITE)), "PUT")).Queries("versioning", "")
  193. // ListObjectsV2
  194. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV2Handler, ACTION_LIST)), "LIST")).Queries("list-type", "2")
  195. // buckets with query
  196. // PutBucketOwnershipControls
  197. bucket.Methods("PUT").HandlerFunc(track(s3a.iam.Auth(s3a.PutBucketOwnershipControls, ACTION_ADMIN), "PUT")).Queries("ownershipControls", "")
  198. //GetBucketOwnershipControls
  199. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.GetBucketOwnershipControls, ACTION_READ), "GET")).Queries("ownershipControls", "")
  200. //DeleteBucketOwnershipControls
  201. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.DeleteBucketOwnershipControls, ACTION_ADMIN), "DELETE")).Queries("ownershipControls", "")
  202. // raw buckets
  203. // PostPolicy
  204. bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST"))
  205. // HeadBucket
  206. bucket.Methods("HEAD").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadBucketHandler, ACTION_READ)), "GET"))
  207. // PutBucket
  208. bucket.Methods("PUT").HandlerFunc(track(s3a.PutBucketHandler, "PUT"))
  209. // DeleteBucket
  210. bucket.Methods("DELETE").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketHandler, ACTION_WRITE)), "DELETE"))
  211. // ListObjectsV1 (Legacy)
  212. bucket.Methods("GET").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV1Handler, ACTION_LIST)), "LIST"))
  213. // raw buckets
  214. }
  215. // ListBuckets
  216. apiRouter.Methods("GET").Path("/").HandlerFunc(track(s3a.ListBucketsHandler, "LIST"))
  217. // NotFound
  218. apiRouter.NotFoundHandler = http.HandlerFunc(s3err.NotFoundHandler)
  219. }