sidekiq-encrypted_args 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3d478d88a4ebaca82ad336b50e60ceebbdf9da240c4c830b3d8bc8ddb6c972d1
4
+ data.tar.gz: 900d93f22766a04db7ab649b2ff7318a2dbb89cb76dd61a732f8e87807308473
5
+ SHA512:
6
+ metadata.gz: ee85740768b20d08a6caf000f84bbe94c9badc46de5637b753f23420ed299b82cccb969d8a87195e9be8c4323d3dcecbe38a2c71a458eaf4864a4932e2b43362
7
+ data.tar.gz: 4f7c7a5894368925e2d59cdcea8eeba6da98f2bf6c3a2cfaf24e99fea3903e203e0fab3039a338236582ae74bef6d114b70869572f1fcb5df6f0eecdd1d1b45e
@@ -0,0 +1,3 @@
1
+ # 1.0.0
2
+
3
+ * Initial release
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Brian Durand
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ # Sidekiq Encrypted Args
2
+
3
+ ![Continuous Integration](https://github.com/bdurand/sidekiq-encrypted_args/workflows/Continuous%20Integration/badge.svg?branch=master)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/70ab3782e4d5285eb173/maintainability)](https://codeclimate.com/github/bdurand/sidekiq-encrypted_args/maintainability)
5
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
+
7
+ Support for encrypting arguments for [Sidekiq](https://github.com/mperham/sidekiq).
8
+
9
+ ## Problem
10
+
11
+ Sidekiq stores the arguments for jobs as JSON in Redis. If your workers include sensitive information (API keys, passwords, personally identifiable information, etc.), you run the risk of accidentally exposing this information. Job arguments are visible in the Sidekiq web interface and your security will only be as good as your Redis server security.
12
+
13
+ This can be an even bigger issue if you use scheduled jobs since sensitive data on those jobs will live in Redis until the job is run. Data written to Redis can also be persisted to disk and live on long after the data in Redis has been deleted.
14
+
15
+ ## Solution
16
+
17
+ This gem adds Sidekiq middleware that allows you to specify job arguments for your workers that should be encrypted in Redis. You do this by adding `encrypted_args` to the `sidekiq_options` in the worker. Jobs for these workers will have their arguments encrypted in Redis and decrypted when passed to `perform` method.
18
+
19
+ To use the gem, you will need to specify a secret that will be used to encrypt the arguments as well as add the middleware to your Sidekiq client and server middleware stacks. You can set that up by adding this to the end of your Sidekiq initialization:
20
+
21
+ ```ruby
22
+ Sidekiq::EncryptedArgs.configure!(secret: "YourSecretKey")
23
+ ```
24
+
25
+ If the secret is not set, the value of the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` environment variable will be used as the secret. If this variable is not set, job arguments will not be encrypted.
26
+
27
+ The call to `Sidekiq::EncryptedArgs.configure!` will append the encryption middleware to the end of the client and server middleware chains. You add the middlewares manually if you need more control over where they appear in the stacks.
28
+
29
+ ```ruby
30
+ Sidekiq::EncryptedArgs.secret = "YourSecretKey"
31
+
32
+ Sidekiq.configure_client do |config|
33
+ config.client_middleware do |chain|
34
+ chain.add Sidekiq::EncryptedArgs::ClientMiddleware
35
+ end
36
+ end
37
+
38
+ Sidekiq.configure_server do |config|
39
+ config.server_middleware do |chain|
40
+ chain.add Sidekiq::EncryptedArgs::ServerMiddleware
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Worker Configuration
46
+
47
+ To declare that a worker is using encrypted arguments, you must set the `encrypted_args` sidekiq option.
48
+
49
+ Setting the option to `true` will encrypt all the arguments passed to the `perform` method.
50
+
51
+ ```ruby
52
+ class SecretWorker
53
+ include Sidekiq::Worker
54
+
55
+ sidekiq_options encrypted_args: true
56
+
57
+ def perform(arg_1, arg_2, arg_3)
58
+ end
59
+ end
60
+ ```
61
+
62
+ You can also encrypt just specific arguments with a hash or an array. This can be useful to preserve visibility into non-sensitive arguments that might be useful for troubleshooting or other reasons. All of these examples will encrypt just the second argument to the `perform` method.
63
+
64
+ ```ruby
65
+ # Pass in a list of argument names that should be encrypted
66
+ sidekiq_options encrypted_args: [:arg_2]
67
+
68
+ def perform(arg_1, arg_2, arg_3)
69
+ end
70
+ ```
71
+
72
+ ```ruby
73
+ # Pass in a hash with values indicating which arguments should be encrypted
74
+ sidekiq_options encrypted_args: { arg_2: true, arg_1: false }
75
+
76
+ def perform(arg_1, arg_2, arg_3)
77
+ end
78
+ ```
79
+
80
+ ```ruby
81
+ # Pass in an array of boolean values indicating which argument positions should be encrypted
82
+ sidekiq_options encrypted_args: [false, true]
83
+
84
+ def perform(arg_1, arg_2, arg_3)
85
+ end
86
+ ```
87
+
88
+ You don't need to change anything else about your workers. All of the arguments passed to the `perform` method will already be unencrypted when the method is called.
89
+
90
+ ## Rolling Secrets
91
+
92
+ If you need to roll your secret, you can simply provide an array when setting the secret.
93
+
94
+ ```ruby
95
+ Sidekiq::EncryptedArgs.secret = ["CurrentSecret", "OldSecret"]
96
+ ```
97
+
98
+ The left most key will be considered the current key and will be used for encrypting arguments. However, all of the keys will be tried when decrypting. This allows you to switch you secret keys without breaking jobs already enqueued in Redis.
99
+
100
+ If you are using the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` envrionment variable to specify your secret, you can delimit multiple keys with a spaces.
101
+
102
+ You can also safely add encryption to an existing worker. Any jobs that are already enqueued will still run even without having the arguments encrypted in Redis.
103
+
104
+ ## Encryption
105
+
106
+ Encrypted arguments are stored using AES-256-GCM with a key derived from your secret using PBKDF2.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ require "benchmark"
6
+ require "sidekiq"
7
+ require_relative "lib/sidekiq/encrypted_args"
8
+
9
+ class WorkerWithoutEncryption
10
+ include Sidekiq::Worker
11
+
12
+ def perform(arg_1, arg_2, arg_3)
13
+ end
14
+ end
15
+
16
+ class WorkerWithEncryption
17
+ include Sidekiq::Worker
18
+
19
+ sidekiq_options encrypted_args: [true, true, true]
20
+
21
+ def perform(arg_1, arg_2, arg_3)
22
+ end
23
+ end
24
+
25
+ middleware = ->(worker_class) {
26
+ job = {"args" => ["foo", "bar", "baz"]}
27
+ Sidekiq::EncryptedArgs::ClientMiddleware.new.call(worker_class, job, "default") do
28
+ worker = worker_class.new
29
+ Sidekiq::EncryptedArgs::ServerMiddleware.new.call(worker, job, "default") do
30
+ worker.perform(*job["args"])
31
+ end
32
+ end
33
+ }
34
+
35
+ Benchmark.bm do |benchmark|
36
+ benchmark.report("No Encryption: ") { 10000.times { middleware.call(WorkerWithoutEncryption) } }
37
+ benchmark.report("With Encryption:") { 10000.times { middleware.call(WorkerWithEncryption) } }
38
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sidekiq/encrypted_args"
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "secret_keys"
5
+ require "sidekiq"
6
+
7
+ module Sidekiq
8
+ module EncryptedArgs
9
+ # Error thrown when the
10
+ class InvalidSecretError < StandardError
11
+ end
12
+
13
+ class << self
14
+ # Set the secret key used for encrypting arguments. If this is not set,
15
+ # the value will be loaded from the `SIDEKIQ_ENCRYPTED_ARGS_SECRET` environment
16
+ # variable. If that value is not set, arguments will not be encypted.
17
+ #
18
+ # @param [String] value One or more secrets to use for encypting arguments.
19
+ #
20
+ # @note You can set multiple secrets by passing an array if you need to roll your secrets.
21
+ # The left most value in the array will be used as the encryption secret, but
22
+ # all the values will be tried when decrypting. That way if you have scheduled
23
+ # jobs that were encypted with a different secret, you can still make it available
24
+ # when decrypting the arguments when the job gets run. If you are using the
25
+ # envrionment variable, separate the keys with spaces.
26
+ def secret=(value)
27
+ @encryptors = make_encryptors(value)
28
+ end
29
+
30
+ # Calling this method will add the client and server middleware to the Sidekiq
31
+ # middleware chains. If you need to ensure the order of where the middleware is
32
+ # added, you can forgo this method and add it yourself.
33
+ def configure!(secret: nil)
34
+ self.secret = secret unless secret.nil?
35
+
36
+ Sidekiq.configure_client do |config|
37
+ config.client_middleware do |chain|
38
+ chain.add Sidekiq::EncryptedArgs::ClientMiddleware
39
+ end
40
+ end
41
+
42
+ Sidekiq.configure_server do |config|
43
+ config.server_middleware do |chain|
44
+ chain.add Sidekiq::EncryptedArgs::ServerMiddleware
45
+ end
46
+ end
47
+ end
48
+
49
+ # Encrypt a value.
50
+ #
51
+ # @param [Object] data Data to encrypt. You can pass any JSON compatible data types or structures.
52
+ #
53
+ # @return [String]
54
+ def encrypt(data)
55
+ return nil if data.nil?
56
+ json = JSON.dump(data)
57
+ encrypted = encrypt_string(json)
58
+ if encrypted == json
59
+ data
60
+ else
61
+ encrypted
62
+ end
63
+ end
64
+
65
+ # Decrypt data
66
+ #
67
+ # @param [String] encrypted_data Data that was previously encrypted. If the value passed in is
68
+ # an unencrypted string, then the string itself will be returned.
69
+ #
70
+ # @return [String]
71
+ def decrypt(encrypted_data)
72
+ return encrypted_data unless SecretKeys::Encryptor.encrypted?(encrypted_data)
73
+ json = decrypt_string(encrypted_data)
74
+ JSON.parse(json)
75
+ end
76
+
77
+ protected
78
+
79
+ # Helper method to get the encrypted args option from an options hash. The value of this option
80
+ # can be `true` or an array indicating if each positional argument should be encrypted, or a hash
81
+ # with keys for the argument position and true as the value.
82
+ def encrypted_args_option(worker_class)
83
+ sidekiq_options = worker_class.sidekiq_options
84
+ option = sidekiq_options.fetch(:encrypted_args, sidekiq_options["encrypted_args"])
85
+
86
+ return nil if option.nil?
87
+
88
+ return Hash.new(true) if option == true
89
+
90
+ return replace_argument_positions(worker_class, option) if option.is_a?(Hash)
91
+
92
+ hash = {}
93
+ Array(option).each_with_index do |val, position|
94
+ if val.is_a?(Symbol) || val.is_a?(String)
95
+ hash[val] = true
96
+ else
97
+ hash[position] = val
98
+ end
99
+ end
100
+ replace_argument_positions(worker_class, hash)
101
+ end
102
+
103
+ private
104
+
105
+ # Hard coded password salt used sent to the encryptor. Do no change.
106
+ SALT = "3270e054"
107
+ private_constant :SALT
108
+
109
+ def encrypt_string(value)
110
+ encryptor = encryptors.first
111
+ return value if encryptor.nil?
112
+ encryptor.encrypt(value)
113
+ end
114
+
115
+ def decrypt_string(value)
116
+ return value if encryptors == [nil]
117
+ encryptors.each do |encryptor|
118
+ begin
119
+ return encryptor.decrypt(value) if encryptor
120
+ rescue OpenSSL::Cipher::CipherError
121
+ # Not the right key, try the next one
122
+ end
123
+ end
124
+ raise InvalidSecretError
125
+ end
126
+
127
+ def encryptors
128
+ if !defined?(@encryptors) || @encryptors.empty?
129
+ @encryptors = make_encryptors(ENV["SIDEKIQ_ENCRYPTED_ARGS_SECRET"].to_s.split)
130
+ if @encryptors.empty? && Sidekiq.logger
131
+ Sidekiq.logger.warn("#{self}: Secret not set for encrypting Sidekiq arguments; arguments will not be encrypted.")
132
+ end
133
+ end
134
+ @encryptors
135
+ end
136
+
137
+ def make_encryptors(secrets)
138
+ Array(secrets).map { |val| val.nil? ? nil : SecretKeys::Encryptor.from_password(val, SALT) }
139
+ end
140
+
141
+ def replace_argument_positions(worker_class, encrypt_option)
142
+ updated = {}
143
+ encrypt_option.each do |key, value|
144
+ if key.is_a?(Symbol) || key.is_a?(String)
145
+ key = key.to_sym
146
+ position = worker_class.instance_method(:perform).parameters.find_index { |_, name| name == key }
147
+ updated[position] = value if position
148
+ elsif key.is_a?(Integer)
149
+ updated[key] = value
150
+ end
151
+ end
152
+ updated
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ require_relative "encrypted_args/client_middleware"
159
+ require_relative "encrypted_args/server_middleware"
160
+ require_relative "encrypted_args/version"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module EncryptedArgs
5
+ # Sidekiq client middleware for encrypting arguments on jobs for workers
6
+ # with `encrypted_args` set in the `sidekiq_options`.
7
+ class ClientMiddleware
8
+ def call(worker_class, job, queue, redis_pool = nil)
9
+ encrypted_args = EncryptedArgs.send(:encrypted_args_option, worker_class)
10
+ if encrypted_args
11
+ new_args = []
12
+ job["args"].each_with_index do |value, position|
13
+ value = EncryptedArgs.encrypt(value) if encrypted_args[position]
14
+ new_args << value
15
+ end
16
+ job["args"] = new_args
17
+ end
18
+
19
+ yield
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module EncryptedArgs
5
+ class ServerMiddleware
6
+ # Sidekiq server middleware for encrypting arguments on jobs for workers
7
+ # with `encrypted_args` set in the `sidekiq_options`.
8
+ def call(worker, job, queue)
9
+ encrypted_args = EncryptedArgs.send(:encrypted_args_option, worker.class)
10
+ if encrypted_args
11
+ new_args = []
12
+ job["args"].each_with_index do |value, position|
13
+ value = EncryptedArgs.decrypt(value) if encrypted_args[position]
14
+ new_args << value
15
+ end
16
+ job["args"] = new_args
17
+ end
18
+
19
+ yield
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module EncryptedArgs
5
+ VERSION = File.read(File.join(__dir__, "..", "..", "..", "VERSION")).chomp.freeze
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "sidekiq-encrypted_args"
3
+ spec.version = File.read(File.join(__dir__, "VERSION")).strip
4
+ spec.authors = ["Brian Durand", "Winston Durand"]
5
+ spec.email = ["bbdurand@gmail.com", "me@winstondurand.com"]
6
+
7
+ spec.summary = "Support for encrypting arguments that contain sensitive information in sidekiq jobs."
8
+ spec.homepage = "https://github.com/bdurand/sidekiq-encrypted_args"
9
+ spec.license = "MIT"
10
+
11
+ # Specify which files should be added to the gem when it is released.
12
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
+ ignore_files = %w[
14
+ .
15
+ Appraisals
16
+ Gemfile
17
+ Gemfile.lock
18
+ Rakefile
19
+ gemfiles/
20
+ spec/
21
+ ]
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
24
+ end
25
+
26
+ spec.require_paths = ["lib"]
27
+ spec.bindir = "bin"
28
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
29
+
30
+ spec.required_ruby_version = ">= 2.4"
31
+
32
+ spec.add_dependency "sidekiq", ">= 4.0"
33
+ spec.add_dependency "secret_keys"
34
+
35
+ spec.add_development_dependency "bundler", "~>2.0"
36
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-encrypted_args
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ - Winston Durand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-06-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sidekiq
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '4.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: secret_keys
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.0'
56
+ description:
57
+ email:
58
+ - bbdurand@gmail.com
59
+ - me@winstondurand.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - CHANGE_LOG.md
65
+ - MIT_LICENSE.txt
66
+ - README.md
67
+ - VERSION
68
+ - benchmark.rb
69
+ - lib/sidekiq-encrypted_args.rb
70
+ - lib/sidekiq/encrypted_args.rb
71
+ - lib/sidekiq/encrypted_args/client_middleware.rb
72
+ - lib/sidekiq/encrypted_args/server_middleware.rb
73
+ - lib/sidekiq/encrypted_args/version.rb
74
+ - sidekiq-encrypted_args.gemspec
75
+ homepage: https://github.com/bdurand/sidekiq-encrypted_args
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '2.4'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.0.3
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Support for encrypting arguments that contain sensitive information in sidekiq
98
+ jobs.
99
+ test_files: []