secvault 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67a57fa1ebe481bab8c9f5f4d9f6d0af27250e4d2c145641c125cd61f88018d0
4
+ data.tar.gz: 8613ed6d87bf067ad69bde7d80d8623a30b6dfb36280ddfe431b236198e63e2c
5
+ SHA512:
6
+ metadata.gz: 3013b492cec7b62296815ea239dc398de08806532ca995de383324a62fd9689e44ae62c744365dbcd29e1158b94b204f2b1522986c5e2d9b1e0d024cab3c871d
7
+ data.tar.gz: 2c74fa262a228253393a7b5818cb6d59554b67fb6ecbfac37d018c271fea9d6ca38d6c8b2cd84d253e16c42d4b28144c6059a1ee5e297d950db2954de6a5285b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2025-09-22
4
+
5
+ ### Added
6
+
7
+ - Initial release of Secvault gem
8
+ - Rails secrets.yml functionality for Rails 7.2+
9
+ - Encrypted secrets.yml support using Rails' built-in encryption
10
+ - Environment-specific secrets management
11
+ - ERB template support in secrets files
12
+ - Rake tasks for secrets management:
13
+ - `rake secvault:setup` - Create encrypted secrets file
14
+ - `rake secvault:edit` - Edit encrypted secrets
15
+ - `rake secvault:show` - Display decrypted secrets
16
+ - Rails generator for creating secrets files
17
+ - Automatic integration with Rails.application.secrets
18
+ - Support for both encrypted and plain YAML secrets files
19
+ - Key management with config/secrets.yml.key
20
+ - Environment variable fallback for encryption key
21
+ - Comprehensive error handling for missing/invalid keys
22
+ - Full test coverage with RSpec
23
+ - Detailed documentation and usage examples
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Unnikrishnan KP
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.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Secvault
2
+
3
+ **Secvault** restores the classic Rails `secrets.yml` functionality that was removed in Rails 7.2, allowing you to manage encrypted secrets using the familiar YAML-based approach.
4
+
5
+ ## Why Secvault?
6
+
7
+ Rails 7.2 removed the `secrets.yml` functionality completely in favor of credentials. However, many teams prefer the simplicity and familiarity of `secrets.yml` for managing environment-specific secrets. Secvault brings this functionality back with modern encryption support.
8
+
9
+ ## Features
10
+
11
+ - 🔐 **Encrypted secrets.yml** - Uses Rails' built-in encryption system
12
+ - 🔑 **Key management** - Secure key generation and management
13
+ - 🌍 **Environment-specific** - Different secrets for development, test, and production
14
+ - 📝 **ERB support** - Use ERB templates in your secrets files
15
+ - 🛠️ **Rake tasks** - Easy management with built-in rake tasks
16
+ - 🚀 **Rails 7.2+ compatible** - Works seamlessly with modern Rails
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'secvault'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ $ bundle install
30
+ ```
31
+
32
+ ## Setup
33
+
34
+ ### 1. Generate secrets file
35
+
36
+ Run the setup task to create your encrypted `secrets.yml`:
37
+
38
+ ```bash
39
+ $ rake secvault:setup
40
+ ```
41
+
42
+ This will:
43
+ - Generate `config/secrets.yml.key` (keep this secure!)
44
+ - Create an encrypted `config/secrets.yml` with default content
45
+ - Remind you to add the key file to `.gitignore`
46
+
47
+ Alternatively, use the Rails generator:
48
+
49
+ ```bash
50
+ $ rails generate secvault:secrets
51
+ ```
52
+
53
+ ### 2. Add key to .gitignore
54
+
55
+ Ensure your encryption key is not committed to version control:
56
+
57
+ ```bash
58
+ echo "/config/secrets.yml.key" >> .gitignore
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Editing secrets
64
+
65
+ Edit your encrypted secrets file:
66
+
67
+ ```bash
68
+ $ rake secvault:edit
69
+ ```
70
+
71
+ This opens the decrypted file in your `$EDITOR`.
72
+
73
+ ### Viewing secrets
74
+
75
+ View the decrypted content:
76
+
77
+ ```bash
78
+ $ rake secvault:show
79
+ ```
80
+
81
+ ### Accessing secrets in your application
82
+
83
+ Secrets are automatically loaded into `Rails.application.secrets`:
84
+
85
+ ```ruby
86
+ # In your Rails application
87
+ Rails.application.secrets.secret_key_base
88
+ Rails.application.secrets.api_key
89
+ Rails.application.secrets.database_password
90
+ ```
91
+
92
+ ### Example secrets.yml structure
93
+
94
+ ```yaml
95
+ # config/secrets.yml (encrypted)
96
+ development:
97
+ secret_key_base: your_development_secret
98
+ api_key: dev_api_key
99
+ database_password: dev_password
100
+
101
+ test:
102
+ secret_key_base: your_test_secret
103
+ api_key: test_api_key
104
+ database_password: test_password
105
+
106
+ production:
107
+ secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
108
+ api_key: <%= ENV['API_KEY'] %>
109
+ database_password: <%= ENV['DATABASE_PASSWORD'] %>
110
+ ```
111
+
112
+ ### Environment variable fallback
113
+
114
+ You can set the encryption key via environment variable:
115
+
116
+ ```bash
117
+ export RAILS_SECRETS_KEY=your_encryption_key
118
+ ```
119
+
120
+ ## Production Deployment
121
+
122
+ ### Option 1: Environment Variable
123
+
124
+ Set the encryption key as an environment variable:
125
+
126
+ ```bash
127
+ export RAILS_SECRETS_KEY=your_encryption_key
128
+ ```
129
+
130
+ ### Option 2: Key File
131
+
132
+ Securely copy `config/secrets.yml.key` to your production server.
133
+
134
+ ### Docker
135
+
136
+ For Docker deployments, you can pass the key as an environment variable:
137
+
138
+ ```dockerfile
139
+ ENV RAILS_SECRETS_KEY=your_encryption_key
140
+ ```
141
+
142
+ ## Rake Tasks
143
+
144
+ | Task | Description |
145
+ |------|-------------|
146
+ | `rake secvault:setup` | Create encrypted secrets.yml and key |
147
+ | `rake secvault:edit` | Edit the encrypted secrets file |
148
+ | `rake secvault:show` | Display decrypted secrets content |
149
+
150
+ ## Migration from Rails < 7.2
151
+
152
+ If you're upgrading from an older Rails version that had `secrets.yml`:
153
+
154
+ 1. Install secvault: `bundle add secvault`
155
+ 2. Encrypt existing secrets: `rake secvault:setup`
156
+ 3. Copy your existing secrets content using `rake secvault:edit`
157
+ 4. Remove the old plain-text `config/secrets.yml`
158
+
159
+ ## Security Best Practices
160
+
161
+ - ✅ **Never commit** `config/secrets.yml.key` to version control
162
+ - ✅ **Use environment variables** for production secrets when possible
163
+ - ✅ **Rotate keys** periodically
164
+ - ✅ **Use strong, unique keys** for each environment
165
+ - ✅ **Limit access** to key files in production
166
+
167
+ ## Troubleshooting
168
+
169
+ ### Missing Key Error
170
+
171
+ ```
172
+ Missing encryption key to decrypt secrets.yml
173
+ ```
174
+
175
+ **Solution**: Ensure `config/secrets.yml.key` exists or set `RAILS_SECRETS_KEY` environment variable.
176
+
177
+ ### Invalid Key Error
178
+
179
+ ```
180
+ Invalid encryption key for secrets.yml
181
+ ```
182
+
183
+ **Solution**: The key doesn't match the encrypted file. Verify you're using the correct key.
184
+
185
+ ### File Not Found
186
+
187
+ ```
188
+ Secrets file doesn't exist
189
+ ```
190
+
191
+ **Solution**: Run `rake secvault:setup` to create the secrets file.
192
+
193
+ ## Development
194
+
195
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
196
+
197
+ To install this gem onto your local machine, run `bundle exec rake install`.
198
+
199
+ ## Contributing
200
+
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/unnitallman/secvault.
202
+
203
+ ## License
204
+
205
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
206
+
207
+ ## Code of Conduct
208
+
209
+ Everyone interacting in the Secvault project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/secvault/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "active_support/encrypted_file"
5
+ require "securerandom"
6
+
7
+ module Secvault
8
+ module Generators
9
+ class SecretsGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Creates a secrets.yml file for encrypted secrets management"
13
+
14
+ class_option :force, type: :boolean, default: false, desc: "Overwrite existing secrets.yml"
15
+
16
+ def create_secrets_file
17
+ secrets_path = Rails.root.join("config/secrets.yml")
18
+ key_path = Rails.root.join("config/secrets.yml.key")
19
+
20
+ if secrets_path.exist? && !options[:force]
21
+ say "Secrets file already exists at config/secrets.yml", :yellow
22
+ return
23
+ end
24
+
25
+ # Generate encryption key
26
+ unless key_path.exist?
27
+ key = ActiveSupport::EncryptedFile.generate_key
28
+ File.write(key_path, key)
29
+ say "Generated encryption key in config/secrets.yml.key", :green
30
+ end
31
+
32
+ # Create encrypted secrets file with template
33
+ encrypted_file = ActiveSupport::EncryptedFile.new(
34
+ content_path: secrets_path,
35
+ key_path: key_path,
36
+ env_key: "RAILS_SECRETS_KEY",
37
+ raise_if_missing_key: true
38
+ )
39
+
40
+ # Write default content
41
+ default_content = generate_default_secrets
42
+ encrypted_file.write(default_content)
43
+
44
+ say "Created encrypted secrets.yml file", :green
45
+ say "Add config/secrets.yml.key to your .gitignore file", :yellow
46
+ end
47
+
48
+ def add_to_gitignore
49
+ gitignore_path = Rails.root.join(".gitignore")
50
+ key_entry = "/config/secrets.yml.key"
51
+
52
+ if gitignore_path.exist?
53
+ gitignore_content = File.read(gitignore_path)
54
+ unless gitignore_content.include?(key_entry)
55
+ File.open(gitignore_path, "a") do |f|
56
+ f.puts "\n# Ignore encrypted secrets key"
57
+ f.puts key_entry
58
+ end
59
+ say "Added secrets key to .gitignore", :green
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def generate_default_secrets
67
+ <<~YAML
68
+ # Be sure to restart your server when you modify this file.
69
+
70
+ # Your secret key is used for verifying the integrity of signed cookies.
71
+ # If you change this key, all old signed cookies will become invalid!
72
+
73
+ # Make sure the secret is at least 30 characters and all random,
74
+ # no regular words or you'll be exposed to dictionary attacks.
75
+ # You can use `rails secret` to generate a secure secret key.
76
+
77
+ # Make sure the secrets in this file are kept private
78
+ # if you're sharing your code publicly.
79
+
80
+ development:
81
+ secret_key_base: #{SecureRandom.hex(64)}
82
+
83
+ test:
84
+ secret_key_base: #{SecureRandom.hex(64)}
85
+
86
+ # Do not keep production secrets in the repository,
87
+ # instead read values from the environment.
88
+ production:
89
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
90
+ YAML
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Secvault
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :secvault
8
+
9
+ initializer "secvault.initialize" do |app|
10
+ Secvault::Secrets.setup(app)
11
+ end
12
+
13
+ generators do
14
+ require "secvault/generators/secrets_generator"
15
+ end
16
+
17
+ rake_tasks do
18
+ load "secvault/tasks.rake"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/encrypted_file"
4
+ require "active_support/core_ext/hash/keys"
5
+ require "active_support/core_ext/object/blank"
6
+ require "pathname"
7
+ require "erb"
8
+ require "yaml"
9
+
10
+ module Secvault
11
+ class Secrets
12
+ class << self
13
+ def setup(app)
14
+ secrets_path = app.root.join("config/secrets.yml")
15
+ key_path = app.root.join("config/secrets.yml.key")
16
+
17
+ if secrets_path.exist?
18
+ app.config.before_configuration do
19
+ # Set up secrets if they exist
20
+ secrets = read_secrets(secrets_path, key_path, Rails.env)
21
+ Rails.application.secrets.merge!(secrets) if secrets
22
+ end
23
+ end
24
+ end
25
+
26
+ def parse(paths, env:)
27
+ configs = paths.collect do |path|
28
+ if path.exist?
29
+ content = encrypted?(path) ? decrypt(path) : path.read
30
+ YAML.safe_load(ERB.new(content).result, aliases: true) || {}
31
+ else
32
+ {}
33
+ end
34
+ end
35
+
36
+ configs.reverse.reduce do |config, overrides|
37
+ config.deep_merge(overrides)
38
+ end[env] || {}
39
+ end
40
+
41
+ def read_secrets(secrets_path, key_path, env)
42
+ if secrets_path.exist?
43
+ all_secrets = if key_path.exist? || encrypted?(secrets_path)
44
+ # Handle encrypted secrets.yml
45
+ decrypt_secrets(secrets_path, key_path)
46
+ else
47
+ # Handle plain YAML secrets.yml
48
+ YAML.safe_load(ERB.new(secrets_path.read).result, aliases: true)
49
+ end
50
+
51
+ env_secrets = all_secrets[env.to_s]
52
+ return env_secrets.deep_symbolize_keys if env_secrets
53
+ end
54
+
55
+ {}
56
+ end
57
+
58
+ def encrypted?(path)
59
+ # Simple heuristic to detect if file is encrypted
60
+ content = path.read
61
+ # Encrypted files typically contain non-printable characters
62
+ !content.valid_encoding? || content.bytes.any? { |b| b < 32 && b != 10 && b != 13 }
63
+ rescue
64
+ false
65
+ end
66
+
67
+ private
68
+
69
+ def decrypt_secrets(secrets_path, key_path)
70
+ encrypted_file = ActiveSupport::EncryptedFile.new(
71
+ content_path: secrets_path,
72
+ key_path: key_path,
73
+ env_key: "RAILS_SECRETS_KEY",
74
+ raise_if_missing_key: true
75
+ )
76
+
77
+ content = encrypted_file.read
78
+ YAML.safe_load(ERB.new(content).result, aliases: true) if content.present?
79
+ rescue ActiveSupport::EncryptedFile::MissingKeyError
80
+ raise MissingKeyError,
81
+ "Missing encryption key to decrypt secrets.yml. " \
82
+ "Ask your team for your secrets key and put it in config/secrets.yml.key"
83
+ rescue ActiveSupport::EncryptedFile::InvalidMessage
84
+ raise InvalidKeyError,
85
+ "Invalid encryption key for secrets.yml."
86
+ end
87
+
88
+ def decrypt(path)
89
+ key_path = Pathname.new("#{path}.key")
90
+ encrypted_file = ActiveSupport::EncryptedFile.new(
91
+ content_path: path,
92
+ key_path: key_path,
93
+ env_key: "RAILS_SECRETS_KEY",
94
+ raise_if_missing_key: true
95
+ )
96
+ encrypted_file.read
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Secvault
4
+ class SecretsHelper
5
+ def initialize(app)
6
+ @app = app
7
+ @secrets_path = app.root.join("config/secrets.yml")
8
+ @key_path = app.root.join("config/secrets.yml.key")
9
+ end
10
+
11
+ def secrets
12
+ @secrets ||= load_secrets
13
+ end
14
+
15
+ def [](key)
16
+ secrets[key.to_sym]
17
+ end
18
+
19
+ def fetch(key, default = nil)
20
+ secrets.fetch(key.to_sym, default)
21
+ end
22
+
23
+ def key?(key)
24
+ secrets.key?(key.to_sym)
25
+ end
26
+
27
+ def empty?
28
+ secrets.empty?
29
+ end
30
+
31
+ def to_h
32
+ secrets.dup
33
+ end
34
+
35
+ private
36
+
37
+ def load_secrets
38
+ return {} unless @secrets_path.exist?
39
+
40
+ env_secrets = Secvault::Secrets.read_secrets(@secrets_path, @key_path, Rails.env)
41
+ env_secrets || {}
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/encrypted_file"
4
+ require "securerandom"
5
+
6
+ namespace :secvault do
7
+ desc "Setup encrypted secrets.yml file"
8
+ task setup: :environment do
9
+ secrets_path = Rails.root.join("config/secrets.yml")
10
+ key_path = Rails.root.join("config/secrets.yml.key")
11
+
12
+ if secrets_path.exist?
13
+ puts "Secrets file already exists at #{secrets_path}"
14
+ else
15
+ # Generate key if it doesn't exist
16
+ unless key_path.exist?
17
+ key = ActiveSupport::EncryptedFile.generate_key
18
+ File.write(key_path, key)
19
+ puts "Generated encryption key in #{key_path}"
20
+ end
21
+
22
+ # Create encrypted file with default content
23
+ encrypted_file = ActiveSupport::EncryptedFile.new(
24
+ content_path: secrets_path,
25
+ key_path: key_path,
26
+ env_key: "RAILS_SECRETS_KEY",
27
+ raise_if_missing_key: true
28
+ )
29
+
30
+ default_content = <<~YAML
31
+ # Be sure to restart your server when you modify this file.
32
+
33
+ development:
34
+ secret_key_base: #{SecureRandom.hex(64)}
35
+
36
+ test:
37
+ secret_key_base: #{SecureRandom.hex(64)}
38
+
39
+ production:
40
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
41
+ YAML
42
+
43
+ encrypted_file.write(default_content)
44
+ puts "Created encrypted secrets.yml file"
45
+ puts "Add #{key_path} to your .gitignore file"
46
+ end
47
+ end
48
+
49
+ desc "Edit encrypted secrets.yml file"
50
+ task edit: :environment do
51
+ secrets_path = Rails.root.join("config/secrets.yml")
52
+ key_path = Rails.root.join("config/secrets.yml.key")
53
+
54
+ unless secrets_path.exist?
55
+ puts "Secrets file doesn't exist. Run 'rake secvault:setup' first."
56
+ exit 1
57
+ end
58
+
59
+ encrypted_file = ActiveSupport::EncryptedFile.new(
60
+ content_path: secrets_path,
61
+ key_path: key_path,
62
+ env_key: "RAILS_SECRETS_KEY",
63
+ raise_if_missing_key: true
64
+ )
65
+
66
+ encrypted_file.change do |tmp_path|
67
+ system("#{ENV["EDITOR"] || "vi"} #{tmp_path}")
68
+ end
69
+
70
+ puts "Updated #{secrets_path}"
71
+ rescue ActiveSupport::EncryptedFile::MissingKeyError
72
+ puts "Missing encryption key to decrypt secrets.yml."
73
+ puts "Ask your team for your secrets key and put it in #{key_path}"
74
+ rescue ActiveSupport::EncryptedFile::InvalidMessage
75
+ puts "Invalid encryption key for secrets.yml."
76
+ end
77
+
78
+ desc "Show decrypted secrets.yml content"
79
+ task show: :environment do
80
+ secrets_path = Rails.root.join("config/secrets.yml")
81
+ key_path = Rails.root.join("config/secrets.yml.key")
82
+
83
+ unless secrets_path.exist?
84
+ puts "Secrets file doesn't exist. Run 'rake secvault:setup' first."
85
+ exit 1
86
+ end
87
+
88
+ encrypted_file = ActiveSupport::EncryptedFile.new(
89
+ content_path: secrets_path,
90
+ key_path: key_path,
91
+ env_key: "RAILS_SECRETS_KEY",
92
+ raise_if_missing_key: true
93
+ )
94
+
95
+ puts encrypted_file.read
96
+ rescue ActiveSupport::EncryptedFile::MissingKeyError
97
+ puts "Missing encryption key to decrypt secrets.yml."
98
+ puts "Ask your team for your secrets key and put it in #{key_path}"
99
+ rescue ActiveSupport::EncryptedFile::InvalidMessage
100
+ puts "Invalid encryption key for secrets.yml."
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Secvault
4
+ VERSION = "1.0.0"
5
+ end
data/lib/secvault.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "yaml"
5
+ require "erb"
6
+ require "active_support/encrypted_file"
7
+ require "active_support/core_ext/hash/keys"
8
+ require "zeitwerk"
9
+
10
+ require_relative "secvault/version"
11
+
12
+ loader = Zeitwerk::Loader.for_gem
13
+ loader.setup
14
+
15
+ module Secvault
16
+ class Error < StandardError; end
17
+ class MissingKeyError < Error; end
18
+ class InvalidKeyError < Error; end
19
+
20
+ extend self
21
+
22
+ def install!
23
+ return if Rails.env.test? || defined?(Rails::Railtie).nil?
24
+
25
+ require "secvault/railtie"
26
+ end
27
+ end
28
+
29
+ Secvault.install! if defined?(Rails)
data/sig/secvault.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Secvault
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secvault
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Unnikrishnan KP
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
41
+ description: Secvault restores the classic Rails secrets.yml functionality that was
42
+ removed in Rails 7.2, allowing you to manage encrypted secrets using the familiar
43
+ YAML-based approach.
44
+ email:
45
+ - unnikrishnan.kp@bigbinary.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".rspec"
51
+ - ".standard.yml"
52
+ - CHANGELOG.md
53
+ - CODE_OF_CONDUCT.md
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - lib/secvault.rb
58
+ - lib/secvault/generators/secrets_generator.rb
59
+ - lib/secvault/railtie.rb
60
+ - lib/secvault/secrets.rb
61
+ - lib/secvault/secrets_helper.rb
62
+ - lib/secvault/tasks.rake
63
+ - lib/secvault/version.rb
64
+ - sig/secvault.rbs
65
+ homepage: https://github.com/unnitallman/secvault
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ allowed_push_host: https://rubygems.org
70
+ homepage_uri: https://github.com/unnitallman/secvault
71
+ source_code_uri: https://github.com/unnitallman/secvault
72
+ changelog_uri: https://github.com/unnitallman/secvault/blob/main/CHANGELOG.md
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 3.0.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.5.10
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Rails secrets.yml functionality for Rails 7.2+
92
+ test_files: []