diff --git a/Envfile b/Envfile index 05c2a4945..c980e1c3b 100644 --- a/Envfile +++ b/Envfile @@ -164,6 +164,12 @@ variable :TWITCH_WEBHOOK_SECRET, :String, default: "Optional" # (https://api.stackexchange.com/docs) variable :STACK_EXCHANGE_APP_KEY, :String, default: "" +# Vault for storing secrets +# (https://learn.hashicorp.com/vault) +variable :VAULT_ADDR, :String, default: "http://127.0.0.1:8200" +variable :VAULT_TOKEN, :String, default: "" +variable :VAULT_SSL_VERIFY, :Boolean, default: true + group :production do variable :SECRET_KEY_BASE, :String diff --git a/Gemfile b/Gemfile index fb99296df..e04d0e3be 100644 --- a/Gemfile +++ b/Gemfile @@ -107,6 +107,7 @@ gem "twitter", "~> 7.0" # A Ruby interface to the Twitter API gem "uglifier", "~> 4.2" # Uglifier minifies JavaScript files gem "ulid", "~> 1.2" # Universally Unique Lexicographically Sortable Identifier implementation for Ruby gem "validate_url", "~> 1.0" # Library for validating urls in Rails +gem "vault", "~> 0.14" # Used to store secrets gem "webpacker", "~> 5.1.1" # Use webpack to manage app-like JavaScript modules in Rails group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 2726c831f..ff0df4761 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -837,6 +837,8 @@ GEM validate_url (1.0.11) activemodel (>= 3.0.0) public_suffix + vault (0.14.0) + aws-sigv4 vcr (6.0.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -1023,6 +1025,7 @@ DEPENDENCIES uglifier (~> 4.2) ulid (~> 1.2) validate_url (~> 1.0) + vault (~> 0.14) vcr (~> 6.0) web-console (~> 4.0) webdrivers (~> 4.4) diff --git a/app/lib/app_secrets.rb b/app/lib/app_secrets.rb new file mode 100644 index 000000000..99e1110c6 --- /dev/null +++ b/app/lib/app_secrets.rb @@ -0,0 +1,19 @@ +class AppSecrets + def self.[](key) + result = Vault.kv(namespace).read(key)&.data&.fetch(:value) if ApplicationConfig["VAULT_TOKEN"].present? + result ||= ApplicationConfig[key] + + result + rescue Vault::VaultError + ApplicationConfig[key] + end + + def self.[]=(key, value) + Vault.kv(namespace).write(key, value: value) + end + + def self.namespace + ENV["VAULT_SECRET_NAMESPACE"] + end + private_class_method :namespace +end diff --git a/config/initializers/vault.rb b/config/initializers/vault.rb new file mode 100644 index 000000000..70b92c57a --- /dev/null +++ b/config/initializers/vault.rb @@ -0,0 +1,40 @@ +Vault.configure do |config| + # The address of the Vault server, also read as + config.address = ApplicationConfig["VAULT_ADDR"] + + # The policy token to authenticate with Vault + # Each app will get its own policy https://learn.hashicorp.com/vault/getting-started/policies#overview + # Each policy comes with its own token to give the app access to ONLY its secrets + config.token = ApplicationConfig["VAULT_TOKEN"] + + # Mimic Paths for communities + + # Optional - if using the Namespace enterprise feature + # config.namespace = ENV["VAULT_NAMESPACE"] + + # Proxy connection information, also read as ENV["VAULT_PROXY_(thing)"] + # config.proxy_address = "..." + # config.proxy_port = "..." + # config.proxy_username = "..." + # config.proxy_password = "..." + + # Custom SSL PEM, also read as ENV["VAULT_SSL_CERT"] + # config.ssl_pem_file = "/path/on/disk.pem" + + # As an alternative to a pem file, you can provide the raw PEM string, also read in the following order of preference: + # ENV["VAULT_SSL_PEM_CONTENTS_BASE64"] then ENV["VAULT_SSL_PEM_CONTENTS"] + # config.ssl_pem_contents = "-----BEGIN ENCRYPTED..." + + # Use SSL verification, also read as ENV["VAULT_SSL_VERIFY"] + config.ssl_verify = ApplicationConfig["VAULT_SSL_VERIFY"] + + # Timeout the connection after a certain amount of time (seconds), also read + # as ENV["VAULT_TIMEOUT"] + config.timeout = 30 + + # It is also possible to have finer-grained controls over the timeouts, these + # may also be read as environment variables + config.ssl_timeout = 5 + config.open_timeout = 5 + config.read_timeout = 30 +end diff --git a/spec/lib/app_secrets_spec.rb b/spec/lib/app_secrets_spec.rb new file mode 100644 index 000000000..9202de842 --- /dev/null +++ b/spec/lib/app_secrets_spec.rb @@ -0,0 +1,64 @@ +require "rails_helper" + +RSpec.describe AppSecrets, type: :lib do + let(:namespace) { "secret-namespace" } + let(:key) { "SECRET_KEY" } + let(:secret_stub) { instance_double("Vault::Kv", data: { value: 1 }) } + let(:vault_stub) { instance_double("Vault::Kv", read: secret_stub) } + + before do + allow(described_class).to receive(:namespace).and_return(namespace) + allow(Vault).to receive(:kv) { vault_stub } + allow(ApplicationConfig).to receive(:[]) + end + + describe "[]" do + context "with VAULT_TOKEN present" do + before do + allow(ApplicationConfig).to receive(:[]).with("VAULT_TOKEN").and_return("present") + end + + it "fetches keys from Vault" do + described_class[key] + expect(Vault).to have_received(:kv).with(namespace) + expect(vault_stub).to have_received(:read).with(key) + end + + it "fetches keys from ApplicationConfig if not in Vault" do + allow(Vault).to receive(:kv) { instance_double("Vault::Kv", read: nil) } + + described_class[key] + expect(ApplicationConfig).to have_received(:[]).with(key) + end + + it "fetches keys from ApplicationConfig if Vault raises an error" do + allow(Vault).to receive(:kv).and_raise(Vault::VaultError) + + described_class[key] + expect(ApplicationConfig).to have_received(:[]).with(key) + end + end + + context "without VAULT_TOKEN present" do + before { allow(ApplicationConfig).to receive(:[]).with("VAULT_TOKEN").and_return("") } + + it "fetches keys from ApplicationConfig" do + allow(Vault).to receive(:kv) { instance_double("Vault::Kv", read: nil) } + + described_class[key] + expect(ApplicationConfig).to have_received(:[]).with(key) + expect(Vault).not_to have_received(:kv).with(namespace) + end + end + end + + describe "[]=" do + it "sets keys in Vault" do + write_stub = instance_double("Vault::Kv", write: nil) + allow(Vault).to receive(:kv) { write_stub } + + described_class[key] = "secret-value" + expect(write_stub).to have_received(:write).with(key, value: "secret-value") + end + end +end diff --git a/vendor/cache/vault-0.14.0.gem b/vendor/cache/vault-0.14.0.gem new file mode 100644 index 000000000..0387a5a31 Binary files /dev/null and b/vendor/cache/vault-0.14.0.gem differ