From d73f13907b62cb577967ae37576c89aa94e0bf56 Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Mon, 4 Nov 2024 09:41:00 -0500 Subject: [PATCH 1/2] Db encryption (#65) * Add optional field encryption to Domain * Add instructions for db encryption --- README.md | 24 ++++++++++++++++++++++++ app/models/domain.rb | 5 +++++ config/astral.yml | 3 +++ config/credentials.yml.enc | 1 - 4 files changed, 32 insertions(+), 1 deletion(-) delete mode 100644 config/credentials.yml.enc diff --git a/README.md b/README.md index 6a143ab..c32f6cf 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,30 @@ UPPER_CASE). Environment vars will override any values in the config file. Per-environment settings in the config file(development, test, production) will override the shared values for that type. +## Database encryption +The local database can be encrypted, if needed, but requires a bit of setup +and careful retention of a master key. Note that there are performance impacts. + +1. First, create encryption keys for the database: +``` +rails db:encryption:init +``` +Copy the output to your clipboard. + +2. Next, create a `credentials.yml.enc` file: +``` +EDITOR=vi rails credentials:edit +``` +Paste the db encryption key data into this file, save, and exit. + +NB, the credentials file is decoded by a key placed in +`config/master.key`. Be sure to save this file (it is .gitignored)! + +3. Finally, set the following Astral configuration to 'true': +``` + db_encryption: true +``` + ## mTLS connections Astral can be run as an SSL service and can communicate with Vault via SSL. Just set the following values in `config/astral.yml` (or environment) to diff --git a/app/models/domain.rb b/app/models/domain.rb index 72003bb..a7a1ca2 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,6 +1,11 @@ class Domain < ApplicationRecord validates :fqdn, presence: true + if Config[:db_encryption] + encrypts :fqdn, :users, :groups + end + + def groups_array (groups || "").split(",").sort.uniq end diff --git a/config/astral.yml b/config/astral.yml index a52ebef..287e475 100644 --- a/config/astral.yml +++ b/config/astral.yml @@ -1,6 +1,9 @@ # Astral configuration # Note that values can be supplied here or as environment vars (UPPER_CASE). shared: + # Set to true and follow setup guide for encrypted sql database fields + db_encryption: false + vault_token: vault_addr: # if VAULT_ADDR is https with self-signed cert, need to provide diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index 68260f7..0000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -fxqU7YIb3OpXuAinPFAShBIC2wVMPP2qAUCvI3OQVNj0J4Ou6dVYreLmEBHXNiJQgZ9w2tHo2DoIXa3mP2cgj6w/XKDe5QghUaRVL6XBT75xsVd1sfiqMqIHe+LA9Jzq+eVqtZBnBPR+rpbJv8Mc/IvrdS+N+zW44Ox11h9ScGcSflELOzfLDjG9nJt7OF9hoGZpKfq8uKjuAPd/qC3PVR7TDizYpJw4JEwqUtCiG1K6/Hq5DlsJ1sYjr2lRdqkTqWSD0l9YOz2VHm6IZPT9KrpgDg1k6jBEC+mrlX0PecrN/Ppo6sYGrTsJuXPCV2JZNcXQ9VlJyL6UHcj7QY2s1At10O7GlA9fxqu2RvPwI6RPXnuy5RoIr6naFPysVaOLqdaR38keEjSynnPql0UlWhjZO74g--jyA5SY7Kpz1KdBCy--pkajz+uD2OZtKQE6aVmaDg== \ No newline at end of file From 093c4f7e63b5ecc44715c57b89ee8c4263590745 Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Mon, 4 Nov 2024 14:04:11 -0500 Subject: [PATCH 2/2] KV policy creation (#70) * Add better auth error propogation from interactor to controller; added interactor unit test * init * reorg * user_config * removed data * basic policy creation working * removed extraneous policies * changed intial email * moved policy creation * fixed test * added test * rubocop * removed user_config file * cleanup * add identity to interactor calls * move policy/entity methods to matching modules * fix client unit test * Simplify policy/entity usage * remove config_user from Cert (using assign_policy now); fix test * Add test of kv_write; verify policy creation * add kv_delete test * PR changes --------- Co-authored-by: George Jahad --- app/interactors/delete_secret.rb | 2 +- app/interactors/read_secret.rb | 2 +- app/interactors/write_secret.rb | 2 +- app/lib/clients/vault/certificate.rb | 26 ++++----------------- app/lib/clients/vault/entity.rb | 9 ++++++++ app/lib/clients/vault/key_value.rb | 27 +++++++++++++++++++--- app/lib/clients/vault/policy.rb | 11 +++++++++ app/lib/services/key_value.rb | 12 +++++----- test/lib/clients/vault_test.rb | 34 +++++++++++++++++++++++----- 9 files changed, 86 insertions(+), 39 deletions(-) diff --git a/app/interactors/delete_secret.rb b/app/interactors/delete_secret.rb index e864b29..7d346bc 100644 --- a/app/interactors/delete_secret.rb +++ b/app/interactors/delete_secret.rb @@ -1,6 +1,6 @@ class DeleteSecret < ApplicationInteractor def call - Services::KeyValue.delete(context.request.path) + Services::KeyValue.delete(context.identity, context.request.path) ensure audit_log end diff --git a/app/interactors/read_secret.rb b/app/interactors/read_secret.rb index b549276..f3fe466 100644 --- a/app/interactors/read_secret.rb +++ b/app/interactors/read_secret.rb @@ -1,6 +1,6 @@ class ReadSecret < ApplicationInteractor def call - if secret = Services::KeyValue.read(context.request.path) + if secret = Services::KeyValue.read(context.identity, context.request.path) context.secret = secret else context.fail!(message: "Failed to read secret: #{context.request.path}") diff --git a/app/interactors/write_secret.rb b/app/interactors/write_secret.rb index 7d3560a..ec4ea4c 100644 --- a/app/interactors/write_secret.rb +++ b/app/interactors/write_secret.rb @@ -1,6 +1,6 @@ class WriteSecret < ApplicationInteractor def call - if secret = Services::KeyValue.write(context.request.path, context.request.data) + if secret = Services::KeyValue.write(context.identity, context.request.path, context.request.data) context.secret = secret else context.fail!(message: "Failed to store secret") diff --git a/app/lib/clients/vault/certificate.rb b/app/lib/clients/vault/certificate.rb index d215c72..5f82ac5 100644 --- a/app/lib/clients/vault/certificate.rb +++ b/app/lib/clients/vault/certificate.rb @@ -1,11 +1,15 @@ module Clients class Vault module Certificate + extend Policy + + GENERIC_CERT_POLICY_NAME = "astral-generic-cert-policy" + def issue_cert(identity, cert_issue_request) opts = cert_issue_request.attributes # Generate the TLS certificate using the intermediate CA tls_cert = client.logical.write(cert_path, opts) - config_user(identity) + assign_policy(identity, GENERIC_CERT_POLICY_NAME) OpenStruct.new tls_cert.data end @@ -19,17 +23,6 @@ def configure_pki create_generic_cert_policy end - def config_user(identity) - sub = identity.sub - email = identity.email - policies, metadata = get_entity_data(sub) - policies.append(Certificate::GENERIC_CERT_POLICY_NAME).to_set.to_a - put_entity(sub, policies, metadata) - put_entity_alias(sub, email, "oidc") - end - - GENERIC_CERT_POLICY_NAME = "astral-generic-cert-policy" - private def intermediate_ca_mount @@ -131,15 +124,6 @@ def configure_ca enable_templating: true) end - def get_entity_data(sub) - entity = read_entity(sub) - if entity.nil? - [ [], nil ] - else - [ entity.data[:policies], entity.data[:metadata] ] - end - end - def create_generic_cert_policy client.sys.put_policy(GENERIC_CERT_POLICY_NAME, generic_cert_policy) end diff --git a/app/lib/clients/vault/entity.rb b/app/lib/clients/vault/entity.rb index 25b4faa..6216d31 100644 --- a/app/lib/clients/vault/entity.rb +++ b/app/lib/clients/vault/entity.rb @@ -15,6 +15,15 @@ def read_entity(name) def delete_entity(name) client.logical.delete("identity/entity/name/#{name}") end + + def get_entity_data(sub) + entity = read_entity(sub) + if entity.nil? + [ [], nil ] + else + [ entity.data[:policies], entity.data[:metadata] ] + end + end end end end diff --git a/app/lib/clients/vault/key_value.rb b/app/lib/clients/vault/key_value.rb index dee6718..ac02d10 100644 --- a/app/lib/clients/vault/key_value.rb +++ b/app/lib/clients/vault/key_value.rb @@ -1,15 +1,19 @@ module Clients class Vault module KeyValue - def kv_read(path) + extend Policy + + def kv_read(identity, path) client.kv(kv_mount).read(path) end - def kv_write(path, data) + def kv_write(identity, path, data) + create_kv_policy(path) + assign_policy(identity, policy_path(path)) client.logical.write("#{kv_mount}/data/#{path}", data: data) end - def kv_delete(path) + def kv_delete(identity, path) client.logical.delete("#{kv_mount}/data/#{path}") end @@ -28,6 +32,23 @@ def kv_mount def kv_engine_type "kv-v2" end + + + def create_kv_policy(path) + client.sys.put_policy(policy_path(path), kv_policy(path)) + end + + def policy_path(path) + "kv_policy/#{path}" + end + + def kv_policy(path) + policy = <<-EOH + path "#{path}" { + capabilities = ["create", "read", "update", "delete"] + } + EOH + end end end end diff --git a/app/lib/clients/vault/policy.rb b/app/lib/clients/vault/policy.rb index d2b12d0..c3dd0ea 100644 --- a/app/lib/clients/vault/policy.rb +++ b/app/lib/clients/vault/policy.rb @@ -1,12 +1,23 @@ module Clients class Vault module Policy + extend Entity + def rotate_token create_astral_policy token = create_astral_token Clients::Vault.token = token end + def assign_policy(identity, policy_name) + sub = identity.sub + email = identity.email + policies, metadata = get_entity_data(sub) + policies.append(policy_name).uniq! + put_entity(sub, policies, metadata) + put_entity_alias(sub, email, "oidc") + end + private def create_astral_policy diff --git a/app/lib/services/key_value.rb b/app/lib/services/key_value.rb index d2bca9a..9bf2ad4 100644 --- a/app/lib/services/key_value.rb +++ b/app/lib/services/key_value.rb @@ -1,16 +1,16 @@ module Services class KeyValue class << self - def read(path) - impl.kv_read(path) + def read(identity, path) + impl.kv_read(identity, path) end - def write(path, data) - impl.kv_write(path, data) + def write(identity, path, data) + impl.kv_write(identity, path, data) end - def delete(path) - impl.kv_delete(path) + def delete(identity, path) + impl.kv_delete(identity, path) end private diff --git a/test/lib/clients/vault_test.rb b/test/lib/clients/vault_test.rb index ae6f534..cd23027 100644 --- a/test/lib/clients/vault_test.rb +++ b/test/lib/clients/vault_test.rb @@ -68,10 +68,10 @@ class VaultTest < ActiveSupport::TestCase # now has a new token assert_not_equal vault_token, @client.token # ensure we can write with the new token - assert_instance_of Vault::Secret, @client.kv_write("testing/secret", { password: "sicr3t" }) + assert_instance_of Vault::Secret, @client.kv_write(@identity, "testing/secret", { password: "sicr3t" }) end - test "#entity" do + test "entity methods" do entity = @client.read_entity(@entity_name) assert_nil entity @@ -84,7 +84,29 @@ class VaultTest < ActiveSupport::TestCase assert_nil entity end - test "#entity_alias" do + test "kv methods" do + # check kv_write + path = "test/path/#{SecureRandom.hex}" + secret = @client.kv_write(@identity, path, { data: "data" }) + assert_kind_of Vault::Secret, secret + + # check kv_read + read_secret = @client.kv_read(@identity, path) + assert_kind_of Vault::Secret, read_secret + + # check policy is created + entity = @client.read_entity(@identity.sub) + assert_equal "kv_policy/#{path}", entity.data[:policies][0] + + # check kv_delete + del_secret = @client.kv_delete(@identity, path) + assert del_secret + read_secret = @client.kv_read(@identity, path) + assert_nil read_secret + end + + + test "entity_alias methods" do # confirm no entity yet err = assert_raises RuntimeError do @client.read_entity_alias(@entity_name, @alias_name) @@ -112,11 +134,11 @@ class VaultTest < ActiveSupport::TestCase assert_match /no such alias/, err.message end - test "#config_user creates valid entity" do - @client.config_user(@identity) + test ".assign_policy creates valid entity" do + @client.assign_policy(@identity, "test_path") entity = @client.read_entity(@identity.sub) assert entity.data[:policies].any? { |p| - p == @client::Certificate::GENERIC_CERT_POLICY_NAME } + p == "test_path" } assert entity.data[:aliases].any? { |a| a[:mount_type] == "oidc" && a[:name] == @identity.sub } end