dotenv-secretsmanager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +70 -0
- data/lib/dotenv/secretsmanager/configuration.rb +20 -0
- data/lib/dotenv/secretsmanager/errors.rb +9 -0
- data/lib/dotenv/secretsmanager/railtie.rb +17 -0
- data/lib/dotenv/secretsmanager/reference.rb +47 -0
- data/lib/dotenv/secretsmanager/resolver.rb +127 -0
- data/lib/dotenv/secretsmanager/version.rb +7 -0
- data/lib/dotenv/secretsmanager.rb +33 -0
- data/lib/dotenv-secretsmanager.rb +3 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aff4454079cc1d1cbb65a127d128f185343e0f68527fea618e2939282db02a60
|
|
4
|
+
data.tar.gz: 570fb3b58c722c78f5d38ba24657cf299463256e78bd4fffd9832b34e760c330
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f59d0999d3ce01cf9d4cc3a0afcda6eacf66b4ee89ee60d0108b2cf895c1deb9ddc99ea0c45b5b08dc02627edf292b97f788326603f40884a5bcd29efd5114e2
|
|
7
|
+
data.tar.gz: af7e008123e0b98461a01091d49d69deb4c9d994eba3f34c0b6e30e55959b1a6b9bfd0e1a00d5b453a399b804a88965fae5e83892d01e6b007cda74604bf60ca
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 key88sf
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# dotenv-secretsmanager
|
|
2
|
+
|
|
3
|
+
Use AWS Secrets Manager references in your Ruby dotenv files. A `.env` value that
|
|
4
|
+
begins with `aws-sm:` is resolved from AWS Secrets Manager at process boot and
|
|
5
|
+
written into `ENV` in place.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile — require AFTER dotenv-rails so ENV is populated first
|
|
11
|
+
gem "dotenv-rails"
|
|
12
|
+
gem "dotenv-secretsmanager"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Reference syntax
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# whole plaintext secret
|
|
23
|
+
RAILS_MASTER_KEY=aws-sm:myproject/master-key
|
|
24
|
+
|
|
25
|
+
# one key from a JSON secret (all three share a single API call)
|
|
26
|
+
DB_PASSWORD=aws-sm:myproject/prod|db_password
|
|
27
|
+
YELP_SECRET=aws-sm:myproject/prod|yelp_client_secret
|
|
28
|
+
TWILIO_TOKEN=aws-sm:myproject/prod|twilio_auth_token
|
|
29
|
+
|
|
30
|
+
# non-reference values are left untouched
|
|
31
|
+
RAILS_LOG_LEVEL=info
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`<secret-id>` may be a friendly name or a full ARN. The optional `|<json-key>`
|
|
35
|
+
selector is split on the last `|`, so ARNs (full of colons, never pipes) parse
|
|
36
|
+
correctly.
|
|
37
|
+
|
|
38
|
+
## Rails
|
|
39
|
+
|
|
40
|
+
No wiring needed. The railtie resolves references automatically after
|
|
41
|
+
`dotenv-rails` loads and before initializers and `database.yml` run.
|
|
42
|
+
|
|
43
|
+
## Plain Ruby
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require "dotenv/secretsmanager"
|
|
47
|
+
Dotenv::SecretsManager.resolve!(ENV)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
Dotenv::SecretsManager.configure do |c|
|
|
54
|
+
c.on_error = :raise # :raise (default) — aggregate all failures, raise once
|
|
55
|
+
# :warn — log each failure, leave literal in ENV
|
|
56
|
+
c.logger = nil # defaults to Rails.logger if present, else $stderr
|
|
57
|
+
c.client = nil # inject a custom Aws::SecretsManager::Client
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Credentials and region come from the standard AWS SDK credential chain. The gem
|
|
62
|
+
makes zero AWS calls and builds no client when no references are present.
|
|
63
|
+
|
|
64
|
+
## Deployment (AWS Lightsail Container Service)
|
|
65
|
+
|
|
66
|
+
Set only `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION` as
|
|
67
|
+
plaintext deployment env vars (a least-privilege IAM user with
|
|
68
|
+
`secretsmanager:GetSecretValue` scoped to your secrets). Put everything else in
|
|
69
|
+
`.env.production` as `aws-sm:` references. Keep `.env.development` free of
|
|
70
|
+
references so local development needs no AWS access.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dotenv
|
|
4
|
+
module SecretsManager
|
|
5
|
+
class Configuration
|
|
6
|
+
# :raise (default) | :warn
|
|
7
|
+
attr_accessor :on_error
|
|
8
|
+
# nil => Rails.logger if present, else a $stderr Logger (resolved at use time)
|
|
9
|
+
attr_accessor :logger
|
|
10
|
+
# nil => a default Aws::SecretsManager::Client (built lazily, only if needed)
|
|
11
|
+
attr_accessor :client
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@on_error = :raise
|
|
15
|
+
@logger = nil
|
|
16
|
+
@client = nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dotenv/secretsmanager"
|
|
4
|
+
|
|
5
|
+
module Dotenv
|
|
6
|
+
module SecretsManager
|
|
7
|
+
# Auto-resolves aws-sm: references in a Rails app. Runs in the
|
|
8
|
+
# before_configuration phase, which is after dotenv-rails populates ENV
|
|
9
|
+
# (provided this gem is required after dotenv-rails) and before initializers
|
|
10
|
+
# and database.yml consume the values.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
config.before_configuration do
|
|
13
|
+
Dotenv::SecretsManager.resolve!(ENV)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dotenv
|
|
4
|
+
module SecretsManager
|
|
5
|
+
# Parses a single .env value into its Secrets Manager components.
|
|
6
|
+
#
|
|
7
|
+
# aws-sm:<secret-id> => whole secret string
|
|
8
|
+
# aws-sm:<secret-id>|<json-key> => one key from a JSON secret
|
|
9
|
+
#
|
|
10
|
+
# The remainder after the scheme is split on the LAST pipe, so neither a
|
|
11
|
+
# friendly name nor an ARN (which contain no pipe) is ever mis-split.
|
|
12
|
+
class Reference
|
|
13
|
+
SCHEME = "aws-sm:"
|
|
14
|
+
|
|
15
|
+
attr_reader :raw, :secret_id, :json_key
|
|
16
|
+
|
|
17
|
+
def self.reference?(value)
|
|
18
|
+
value.is_a?(String) && value.start_with?(SCHEME)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.parse(value)
|
|
22
|
+
new(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(raw)
|
|
26
|
+
@raw = raw
|
|
27
|
+
remainder = raw[SCHEME.length..] || ""
|
|
28
|
+
|
|
29
|
+
if remainder.include?("|")
|
|
30
|
+
before, _, after = remainder.rpartition("|")
|
|
31
|
+
@secret_id = before
|
|
32
|
+
@json_key = after
|
|
33
|
+
else
|
|
34
|
+
@secret_id = remainder
|
|
35
|
+
@json_key = nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def malformed?
|
|
40
|
+
return true if @secret_id.nil? || @secret_id.empty?
|
|
41
|
+
return true if !@json_key.nil? && @json_key.empty?
|
|
42
|
+
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Dotenv
|
|
6
|
+
module SecretsManager
|
|
7
|
+
class Resolver
|
|
8
|
+
Failure = Struct.new(:env_key, :reference, :reason)
|
|
9
|
+
|
|
10
|
+
# Internal control-flow signal for a single unresolvable reference.
|
|
11
|
+
# Never escapes resolve!; the public type is ResolutionError.
|
|
12
|
+
class ResolutionFailure < StandardError; end
|
|
13
|
+
|
|
14
|
+
def initialize(env:, config:)
|
|
15
|
+
@env = env
|
|
16
|
+
@config = config
|
|
17
|
+
@secret_cache = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve!
|
|
21
|
+
references = collect_references
|
|
22
|
+
return @env if references.empty?
|
|
23
|
+
|
|
24
|
+
failures = []
|
|
25
|
+
references.each do |env_key, reference|
|
|
26
|
+
@env[env_key] = resolve_one(reference)
|
|
27
|
+
rescue ResolutionFailure => e
|
|
28
|
+
failures << Failure.new(env_key, reference, e.message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
handle_failures(failures) unless failures.empty?
|
|
32
|
+
@env
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def collect_references
|
|
38
|
+
@env.keys.each_with_object([]) do |key, acc|
|
|
39
|
+
value = @env[key]
|
|
40
|
+
acc << [key, Reference.parse(value)] if Reference.reference?(value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_one(reference)
|
|
45
|
+
raise ResolutionFailure, "malformed reference" if reference.malformed?
|
|
46
|
+
|
|
47
|
+
secret = fetch(reference.secret_id)
|
|
48
|
+
reference.json_key ? extract_json_key(secret, reference) : secret
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One GetSecretValue per distinct secret-id for the whole pass. A failed
|
|
52
|
+
# fetch is cached as the failure so repeated references neither refetch nor
|
|
53
|
+
# silently succeed.
|
|
54
|
+
def fetch(secret_id)
|
|
55
|
+
@secret_cache[secret_id] = fetch_uncached(secret_id) unless @secret_cache.key?(secret_id)
|
|
56
|
+
|
|
57
|
+
result = @secret_cache[secret_id]
|
|
58
|
+
raise result if result.is_a?(ResolutionFailure)
|
|
59
|
+
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_uncached(secret_id)
|
|
64
|
+
response = client.get_secret_value(secret_id: secret_id)
|
|
65
|
+
string = response.secret_string
|
|
66
|
+
return ResolutionFailure.new("secret '#{secret_id}' has no string value") if string.nil?
|
|
67
|
+
|
|
68
|
+
string
|
|
69
|
+
rescue Aws::Errors::ServiceError => e
|
|
70
|
+
ResolutionFailure.new("AWS error for '#{secret_id}': #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_json_key(secret, reference)
|
|
74
|
+
data =
|
|
75
|
+
begin
|
|
76
|
+
JSON.parse(secret)
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
raise ResolutionFailure, "secret '#{reference.secret_id}' is not valid JSON"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless data.is_a?(Hash)
|
|
82
|
+
raise ResolutionFailure, "secret '#{reference.secret_id}' is not a JSON object"
|
|
83
|
+
end
|
|
84
|
+
unless data.key?(reference.json_key)
|
|
85
|
+
raise ResolutionFailure,
|
|
86
|
+
"key '#{reference.json_key}' not found in secret '#{reference.secret_id}'"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
data.fetch(reference.json_key).to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_failures(failures)
|
|
93
|
+
if @config.on_error == :warn
|
|
94
|
+
failures.each { |f| logger.warn("[dotenv-secretsmanager] #{failure_line(f)}") }
|
|
95
|
+
else
|
|
96
|
+
raise ResolutionError, build_error_message(failures)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def failure_line(failure)
|
|
101
|
+
"#{failure.env_key} (#{failure.reference.raw}): #{failure.reason}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_error_message(failures)
|
|
105
|
+
lines = failures.map { |f| " - #{failure_line(f)}" }
|
|
106
|
+
"Failed to resolve #{failures.size} Secrets Manager reference(s):\n#{lines.join("\n")}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def client
|
|
110
|
+
@client ||= @config.client || Aws::SecretsManager::Client.new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def logger
|
|
114
|
+
@logger ||= @config.logger || default_logger
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def default_logger
|
|
118
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
119
|
+
Rails.logger
|
|
120
|
+
else
|
|
121
|
+
require "logger"
|
|
122
|
+
Logger.new($stderr)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dotenv/secretsmanager/version"
|
|
4
|
+
require "dotenv/secretsmanager/errors"
|
|
5
|
+
require "dotenv/secretsmanager/configuration"
|
|
6
|
+
require "dotenv/secretsmanager/reference"
|
|
7
|
+
require "aws-sdk-secretsmanager"
|
|
8
|
+
require "dotenv/secretsmanager/resolver"
|
|
9
|
+
|
|
10
|
+
module Dotenv
|
|
11
|
+
module SecretsManager
|
|
12
|
+
class << self
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield(configuration) if block_given?
|
|
19
|
+
configuration
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve!(env = ENV)
|
|
27
|
+
Resolver.new(env: env, config: configuration).resolve!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require "dotenv/secretsmanager/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dotenv-secretsmanager
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- key88sf
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: aws-sdk-secretsmanager
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
description: 'Treats .env values beginning with aws-sm: as references to AWS Secrets
|
|
55
|
+
Manager secrets, resolving them into ENV at process boot. Framework-agnostic core
|
|
56
|
+
with an optional Rails railtie.'
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- LICENSE
|
|
62
|
+
- README.md
|
|
63
|
+
- lib/dotenv-secretsmanager.rb
|
|
64
|
+
- lib/dotenv/secretsmanager.rb
|
|
65
|
+
- lib/dotenv/secretsmanager/configuration.rb
|
|
66
|
+
- lib/dotenv/secretsmanager/errors.rb
|
|
67
|
+
- lib/dotenv/secretsmanager/railtie.rb
|
|
68
|
+
- lib/dotenv/secretsmanager/reference.rb
|
|
69
|
+
- lib/dotenv/secretsmanager/resolver.rb
|
|
70
|
+
- lib/dotenv/secretsmanager/version.rb
|
|
71
|
+
homepage: https://github.com/key88sf/dotenv-secretsmanager
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata: {}
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.11
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Resolve AWS Secrets Manager references inside .env files at boot.
|
|
92
|
+
test_files: []
|