147 lines
5.3 KiB
Ruby
147 lines
5.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# WARNING: Before you make a change to secrets.yml, read the development guide for GitLab secrets
|
|
# doc/development/application_secrets.md.
|
|
#
|
|
# This file needs to be loaded BEFORE any initializers that require access
|
|
# to secrets (e.g. config/initializers/asset_proxy_settings.rb).
|
|
#
|
|
# Be sure to restart your server when you modify this file.
|
|
require 'securerandom'
|
|
|
|
class SecretsInitializer
|
|
def initialize(secrets_file_path:, rails_env:)
|
|
@secrets_file_path = secrets_file_path
|
|
@rails_env = rails_env
|
|
end
|
|
|
|
def secrets_from_file
|
|
@secrets_from_file ||= begin
|
|
YAML.safe_load_file(secrets_file_path)
|
|
rescue Errno::ENOENT
|
|
{}
|
|
end
|
|
end
|
|
|
|
def execute!
|
|
set_credentials_from_file_and_env!
|
|
set_missing_from_defaults!
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :secrets_file_path, :rails_env
|
|
|
|
def secrets_for_env(env)
|
|
secrets = secrets_from_file[env] || {}
|
|
secrets.deep_symbolize_keys
|
|
end
|
|
|
|
def set_credentials_from_file_and_env!
|
|
# Inspired by https://github.com/rails/rails/blob/v7.0.8.4/railties/lib/rails/secrets.rb#L25-L36
|
|
# Later, once config/secrets.yml won't be read automatically, we'll need to do it manually, so
|
|
# we anticipate and do it ourselves here.
|
|
secrets = secrets_for_env('shared')
|
|
.merge(secrets_for_env(rails_env))
|
|
|
|
# Copy secrets from config/secrets.yml into Rails.application.credentials
|
|
# If we support native Rails.application.credentials later
|
|
# (e.g. config.credentials.yml.enc + config/master.key ), this loop would
|
|
# become a no-op as long as credentials are migrated to config.credentials.yml.enc.
|
|
secrets.each do |key, value|
|
|
next if Rails.application.credentials.public_send(key).present?
|
|
|
|
Rails.application.credentials[key] = value
|
|
end
|
|
|
|
# Historically, ENV['SECRET_KEY_BASE'] takes precedence over config/secrets.yml, so we maintain that
|
|
# behavior by ensuring the environment variable always overrides the value from config/secrets.yml.
|
|
env_secret_key = ENV['SECRET_KEY_BASE']
|
|
Rails.application.credentials.secret_key_base = env_secret_key if env_secret_key.present?
|
|
end
|
|
|
|
def set_missing_from_defaults!
|
|
defaults = {
|
|
secret_key_base: generate_new_secure_token,
|
|
otp_key_base: generate_new_secure_token,
|
|
db_key_base: generate_new_secure_token,
|
|
openid_connect_signing_key: generate_new_rsa_private_key,
|
|
# 1. We set the following two keys as an array to support keys rotation.
|
|
# The last key in the array is always used to encrypt data:
|
|
# https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/encryption/key_provider.rb#L21
|
|
# while all the keys are used (in the order they're defined) to decrypt data:
|
|
# https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/encryption/cipher.rb#L26.
|
|
# This allows to rotate keys by adding a new key as the last key, and start a re-encryption process that
|
|
# runs in the background: https://gitlab.com/gitlab-org/gitlab/-/issues/494976
|
|
# 2. We use the same method and length as Rails' defaults:
|
|
# https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/railties/databases.rake#L537-L540
|
|
active_record_encryption_primary_key: [generate_new_secure_random_alphanumeric(32)],
|
|
active_record_encryption_deterministic_key: [generate_new_secure_random_alphanumeric(32)],
|
|
active_record_encryption_key_derivation_salt: generate_new_secure_random_alphanumeric(32)
|
|
}
|
|
|
|
# encrypted_settings_key_base is optional for now
|
|
if ENV['GITLAB_GENERATE_ENCRYPTED_SETTINGS_KEY_BASE']
|
|
defaults[:encrypted_settings_key_base] = generate_new_secure_token
|
|
end
|
|
|
|
missing_secrets = set_missing_keys(defaults)
|
|
write_secrets_yml!(missing_secrets) if missing_secrets.any?
|
|
end
|
|
|
|
def generate_new_secure_token
|
|
SecureRandom.hex(64)
|
|
end
|
|
|
|
def generate_new_rsa_private_key
|
|
OpenSSL::PKey::RSA.new(2048).to_pem
|
|
end
|
|
|
|
def generate_new_secure_random_alphanumeric(chars)
|
|
SecureRandom.alphanumeric(chars)
|
|
end
|
|
|
|
def warn_missing_secret(secret)
|
|
return if rails_env.test?
|
|
|
|
warn "Missing Rails.application.credentials.#{secret} for #{rails_env} environment. " \
|
|
"The secret will be generated and stored in config/secrets.yml."
|
|
end
|
|
|
|
def set_missing_keys(defaults)
|
|
defaults.each_with_object({}) do |(key, default), missing|
|
|
next if Rails.application.credentials.public_send(key).present?
|
|
|
|
warn_missing_secret(key)
|
|
|
|
missing[key] = Rails.application.credentials[key] = default
|
|
end
|
|
end
|
|
|
|
def backup_pathname
|
|
@backup_pathname ||= Pathname(Settings.backup.path)
|
|
end
|
|
|
|
def write_secrets_yml!(missing_secrets)
|
|
secrets_from_file[rails_env.to_s] ||= {}
|
|
secrets_from_file[rails_env.to_s].merge!(missing_secrets)
|
|
|
|
if File.exist?(secrets_file_path)
|
|
FileUtils.mkdir_p(backup_pathname)
|
|
|
|
backup_path = backup_pathname.join("#{File.basename(secrets_file_path)}.orig.#{Time.now.to_i}")
|
|
|
|
warn "Creating a backup of secrets file #{secrets_file_path} at #{backup_path}"
|
|
FileUtils.mv(secrets_file_path, backup_path)
|
|
end
|
|
|
|
File.write(
|
|
secrets_file_path,
|
|
YAML.dump(secrets_from_file.deep_stringify_keys),
|
|
mode: 'w', perm: 0o600
|
|
)
|
|
end
|
|
end
|
|
|
|
SecretsInitializer.new(secrets_file_path: Rails.root.join('config/secrets.yml'), rails_env: Rails.env).execute!
|