[deploy] Feature: Setup Vault and AppSecrets Wrapper Class (#8891)

This commit is contained in:
Molly Struve 2020-06-24 14:13:37 -05:00 committed by GitHub
parent 09bb730f9d
commit 0767c34379
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

19
app/lib/app_secrets.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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

BIN
vendor/cache/vault-0.14.0.gem vendored Normal file

Binary file not shown.