-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue 341 modernise secret handling #392
Changes from 20 commits
1ad3bc1
70c7dd4
cde294b
983d868
d100d0c
96f2c05
3a6ef51
8a5492d
7a0a026
0019a73
64fb8f2
4c8981a
59d8370
1633c3c
08737b9
6a7dc47
9461bb2
a93845c
d161f74
d8f3293
944a40e
61a18ba
3247099
6a78028
c5eacb6
8f91295
24f1153
3801920
31acec3
c6f4dc3
7cc48c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Be sure to restart your server when you modify this file. | ||
# | ||
# Use this file to load non-sensitive app config from ENV. Config values here | ||
# will be loaded into `Rails.application.config.app`. | ||
# | ||
# Sensitive config should be put in `config/secrets.yml` (which will load it | ||
# into `Rails.application.secrets`) | ||
|
||
default: &default | ||
# The default `From:` address to use for email sent by this application | ||
# obviously isn't a secret per se, but configuring it here is convenient | ||
mail_from: "<%= ENV['MAIL_FROM'] %>" | ||
eoinkelly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
development: | ||
<<: *default | ||
|
||
test: | ||
<<: *default | ||
|
||
staging: | ||
<<: *default | ||
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐘 ideally we should actually only add this if a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep as is because:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that can be the case because I'm fine with whatever you think is worth doing |
||
|
||
production: | ||
<<: *default |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,21 @@ | |
# the empty line at the beginning of this string is required | ||
<<-'RUBY' | ||
|
||
# load config/app.yml into Rails.application.config.app.* | ||
config.app = config_for(:app) | ||
|
||
config.middleware.insert_before Rack::Sendfile, HttpBasicAuth | ||
config.action_dispatch.default_headers["Permissions-Policy"] = "interest-cohort=()" | ||
|
||
# ActiveRecord encrypted attributes expectes to find the key secrets under | ||
# `config.active_record.encryption.*`. If the secrets were stored in Rails | ||
# encrypted credentials file then Rails would map them automatically for us. | ||
# We prefer to store the secrets in the ENV and load them through | ||
# `config/secrets.yml` so we have to manually assign them here. | ||
config.active_record.encryption.primary_key = Rails.application.secrets.active_record_encryption_primary_key | ||
config.active_record.encryption.deterministic_key = Rails.application.secrets.active_record_encryption_deterministic_key | ||
config.active_record.encryption.key_derivation_salt = Rails.application.secrets.active_record_encryption_key_derivation_salt | ||
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @G-Rath It turns out we can use encrypted attributes without encrypted credentials. It is very poorly documented so I think it's worth baking into the template. |
||
|
||
config.action_dispatch.default_headers["X-Frame-Options"] = "DENY" | ||
RUBY | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,42 @@ | ||
# frozen-string-literal: true | ||
class VerifyPlaceholderSecretsNotUsedForReal | ||
class << self | ||
PLACEHOLDER_PREFIX_REGEX = /(PLACEHOLDER|FAILED_TO_GENERATE)/.freeze | ||
|
||
# RAILS_SECRET_KEY_BASE should be set to something other than its value in example.env | ||
def run | ||
return if local? | ||
|
||
if Rails.env.production? && Rails.root.join("example.env").read.include?(ENV.fetch("RAILS_SECRET_KEY_BASE")) | ||
fail "RAILS_SECRET_KEY_BASE is unchanged from example.env. " \ | ||
"Generate a new one with `bundle exec rails secret`" | ||
verify_secret_key_base | ||
verify_activerecord_encryption_secrets | ||
end | ||
|
||
private | ||
|
||
def verify_secret_key_base | ||
return unless Rails.root.join("example.env").read.include?(ENV.fetch("RAILS_SECRET_KEY_BASE")) | ||
|
||
fail "RAILS_SECRET_KEY_BASE is unchanged from example.env. Generate a new one with `bundle exec rails secret`" | ||
end | ||
|
||
## | ||
# Verify that placeholder values created by the Ackama rails template are | ||
# not being used for real. | ||
# | ||
eoinkelly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def verify_activerecord_encryption_secrets # rubocop:disable Metrics/AbcSize | ||
secrets = [ | ||
Rails.application.config.active_record.encryption.primary_key, | ||
Rails.application.config.active_record.encryption.deterministic_key, | ||
Rails.application.config.active_record.encryption.key_derivation_salt | ||
] | ||
|
||
secrets.each do |secret| | ||
fail "Insecure ENV: ActiveRecored encrypted credentials env contain in insecure placeholder value." if secret.match?(PLACEHOLDER_PREFIX_REGEX) | ||
eoinkelly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
|
||
def local? | ||
Rails.env.development? || Rails.env.test? | ||
end | ||
end | ||
end | ||
|
||
VerifyPlaceholderSecretsNotUsedForReal.run | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @robotdana You may want to review this because I've changed it quite a bit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks good |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Do NOT put secrets directly into this file. All secrets should be loaded from ENV! | ||
# Be sure to restart your server when you modify this file. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐘 I think it would be worth mentioning |
||
|
||
default: &default | ||
# Your secret key is used for verifying the integrity of signed cookies. | ||
# If you change this key, all old signed cookies will become invalid! | ||
# Make sure the secret is at least 30 characters and all random, | ||
# no regular words or you'll be exposed to dictionary attacks. | ||
# You can use `rails secret` to generate a secure secret key. | ||
secret_key_base: "<%= ENV.fetch('RAILS_SECRET_KEY_BASE') %>" | ||
|
||
active_record_encryption_primary_key: "<%= ENV.fetch('ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY') %>" | ||
active_record_encryption_deterministic_key: "<%= ENV.fetch('ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY') %>" | ||
active_record_encryption_key_derivation_salt: "<%= ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT') %>" | ||
|
||
development: | ||
<<: *default | ||
|
||
test: | ||
<<: *default | ||
|
||
staging: | ||
<<: *default | ||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐘 ideally we should actually only add this if a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep as is because:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that can be the case because I'm fine with whatever you think is worth doing |
||
|
||
production: | ||
<<: *default |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,10 @@ | |
|
||
template "variants/backend-base/config/database.yml.tt", "config/database.yml", force: true | ||
|
||
template "variants/backend-base/config/secrets.example.yml.tt", "config/secrets.example.yml" | ||
remove_file "config/secrets.yml" | ||
copy_file "variants/backend-base/config/secrets.yml", "config/secrets.yml", force: true | ||
copy_file "variants/backend-base/config/app.yml", "config/app.yml" | ||
remove_file "config/master.key" | ||
remove_file "config/credentials.yml.enc" | ||
Comment on lines
+7
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We aren't using these but by default Rails creates them and puts a |
||
|
||
copy_file "variants/backend-base/config/puma.rb", "config/puma.rb", force: true | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,3 @@ | ||
# Copy this file into a new file called ".envrc" in the root of the project. | ||
# Access values like this: ENV["RAILS_SECRET_KEY_BASE"] | ||
# | ||
# The purpose of this file is to keep secrets out of source control. | ||
# For more information, see: direnv.net | ||
|
||
# The environment variables below can be uncommented to enable HTTP basic | ||
# authentication | ||
# HTTP_BASIC_AUTH_USERNAME=example | ||
|
@@ -26,3 +20,13 @@ PORT=3000 | |
# SENTRY_DSN=http://public@example.com/project-id | ||
SENTRY_CSP_HEADER_REPORT_ENDPOINT=https://SOMECODE.ingest.sentry.io/api/SOMENUMS/security/?sentry_key=SOMETHING | ||
SENTRY_ENV=development | ||
|
||
# NEVER just copy these secrets from development env to a production env. Run: | ||
# | ||
# bin/rails db:encryption:init | ||
# | ||
# to create new versions of these secrets for each deployed environment (e.g. | ||
# staging, production) | ||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="PLACEHOLDER_PRIMARY_KEY" | ||
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="PLACEHOLDER_DETERMINISTIC_KEY" | ||
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="PLACEHOLDER_KEY_DERIVATION_SALT" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These env vars need to exist to load Rails because we are loading them with
eoinkelly marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
def generate_db_secrets | ||
puts "DB_ENCRYPTION_INIT: Running rails db:encryption:init to set realistic values in ENV variables" | ||
|
||
raw = `bin/rails db:encryption:init` | ||
unparsed_yaml = raw.sub(/Add.+\n/, "") | ||
parsed = YAML.safe_load(unparsed_yaml) | ||
parsed.fetch("active_record_encryption") | ||
rescue StandardError => e | ||
puts "DB_ENCRYPTION_INIT: Recovering from error: #{e.inspect}" | ||
{ | ||
"primary_key" => "FAILED_TO_GENERATE_DEFAULT_PRIMARY_KEY", | ||
"deterministic_key" => "FAILED_TO_GENERATE_DEFAULT_DETERMINISTIC_KEY", | ||
"key_derivation_salt" => "FAILED_TO_GENERATE_DEFAULT_KEY_DERIVATION_SALT" | ||
} | ||
end | ||
|
||
# To avoid setting a bad security example we don't use the same secrets for the | ||
# example.env (which is checked in) and your local .env file. | ||
example_env_db_secrets = generate_db_secrets | ||
dot_env_db_secrets = generate_db_secrets | ||
|
||
gsub_file("example.env", "PLACEHOLDER_PRIMARY_KEY", example_env_db_secrets.fetch("primary_key")) | ||
gsub_file("example.env", "PLACEHOLDER_DETERMINISTIC_KEY", example_env_db_secrets.fetch("deterministic_key")) | ||
gsub_file("example.env", "PLACEHOLDER_KEY_DERIVATION_SALT", example_env_db_secrets.fetch("key_derivation_salt")) | ||
|
||
gsub_file(".env", "PLACEHOLDER_PRIMARY_KEY", dot_env_db_secrets.fetch("primary_key")) | ||
gsub_file(".env", "PLACEHOLDER_DETERMINISTIC_KEY", dot_env_db_secrets.fetch("deterministic_key")) | ||
gsub_file(".env", "PLACEHOLDER_KEY_DERIVATION_SALT", dot_env_db_secrets.fetch("key_derivation_salt")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot load things directly in to
Rails.application.config
using a YAML file - we have to pick a namespace underconfig
. I choseconfig.app
but totally open to changing this.Alternatively we could skip the YAML file completely and just have a ruby initializer which loads things directly into
Rails.application.config
. I slightly favour the YAML file because it makes it easier when secrets vary between environments.