My opinionated ruby on rails template
1# frozen_string_literal: true
2
3say 'Setting up security gems...', :green
4
5gem 'lockbox'
6gem 'blind_index'
7gem 'invisible_captcha'
8gem 'strong_migrations'
9
10after_bundle do
11 say ' Running Lockbox audits generator...', :cyan
12 rails_command 'generate lockbox:audits'
13
14 say ' Running Strong Migrations installer...', :cyan
15 rails_command 'generate strong_migrations:install'
16end
17
18say ' Creating Lockbox initializer...', :cyan
19initializer 'lockbox.rb', <<~RUBY
20 # frozen_string_literal: true
21
22 # Lockbox - Field-level encryption
23 # https://github.com/ankane/lockbox
24 #
25 # Generate key with: Lockbox.generate_key
26
27 if Rails.application.credentials.lockbox&.key?(:master_key)
28 Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
29 elsif ENV['LOCKBOX_MASTER_KEY'].present?
30 Lockbox.master_key = ENV['LOCKBOX_MASTER_KEY']
31 elsif Rails.env.production?
32 raise 'Lockbox master_key not configured! Run: rails credentials:edit'
33 else
34 Rails.logger.warn 'Lockbox master_key not found in credentials. Add it with: rails credentials:edit'
35 end
36RUBY
37
38say ' Creating BlindIndex initializer...', :cyan
39initializer 'blind_index.rb', <<~RUBY
40 # frozen_string_literal: true
41
42 # Blind Index - Searchable Encryption
43 # https://github.com/ankane/blind_index
44 #
45 # Allows searching encrypted columns without decrypting them
46 # Generate key with: BlindIndex.generate_key
47
48 if Rails.application.credentials.blind_index&.key?(:master_key)
49 BlindIndex.master_key = Rails.application.credentials.blind_index[:master_key]
50 elsif ENV['BLIND_INDEX_MASTER_KEY'].present?
51 BlindIndex.master_key = ENV['BLIND_INDEX_MASTER_KEY']
52 elsif Rails.env.production?
53 raise 'BlindIndex master_key not configured! Run: rails credentials:edit'
54 else
55 Rails.logger.warn 'BlindIndex master_key not found in credentials. Add it with: rails credentials:edit'
56 end
57
58 # Default options
59 BlindIndex.default_options[:algorithm] = :argon2id # Most secure, recommended
60 BlindIndex.default_options[:insecure_key] = false # Require secure keys
61RUBY
62
63say ' Creating InvisibleCaptcha initializer...', :cyan
64file 'config/initializers/invisible_captcha.rb', <<~RUBY
65 # frozen_string_literal: true
66
67 InvisibleCaptcha.setup do |config|
68 # Minimum time (in seconds) for a human to fill out a form
69 config.timestamp_threshold = 2
70
71 # Custom honeypot field name (randomized per form by default)
72 # config.honeypots = ['foo', 'bar']
73
74 # Flash message when spam is detected
75 config.timestamp_error_message = 'Something went wrong. Please try again.'
76
77 # Enable visual mode for debugging in development
78 config.visual_honeypots = Rails.env.development?
79 end
80RUBY
81
82say ' Creating Encryptable concern...', :cyan
83file 'app/models/concerns/encryptable.rb', <<~RUBY
84 # frozen_string_literal: true
85
86 # Include this concern and use the DSL to encrypt sensitive fields
87 #
88 # Example:
89 # class User < ApplicationRecord
90 # include Encryptable
91 #
92 # encrypts_field :ssn
93 # encrypts_field :phone, searchable: true
94 # end
95 #
96 # Migration for encrypted fields:
97 # add_column :users, :ssn_ciphertext, :text
98 # add_column :users, :phone_ciphertext, :text
99 # add_column :users, :phone_bidx, :string # for searchable fields
100 # add_index :users, :phone_bidx
101 #
102 module Encryptable
103 extend ActiveSupport::Concern
104
105 class_methods do
106 def encrypts_field(field_name, searchable: false)
107 encrypts field_name
108
109 if searchable
110 blind_index field_name
111 end
112 end
113 end
114 end
115RUBY
116
117say ' Adding invisible_captcha to ApplicationController...', :cyan
118inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY
119 # Invisible captcha is available in forms via: invisible_captcha
120 # Add to specific controllers with: invisible_captcha only: [:create], on_spam: :spam_detected
121 #
122 # private
123 # def spam_detected
124 # redirect_to root_path, alert: 'Spam detected.'
125 # end
126RUBY
127
128say 'Security gems configured!', :green
129say ' - Lockbox: Use `encrypts :field_name` in models', :cyan
130say ' - BlindIndex: Use `blind_index :field_name` for searchable encrypted fields', :cyan
131say ' - InvisibleCaptcha: Use `invisible_captcha` helper in forms', :cyan
132say ' Generate production keys with: Lockbox.generate_key / BlindIndex.generate_key', :yellow