From 3a6afd11e6625f03b41ad34987514d18f154a49e Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 30 Sep 2024 13:09:01 -0400 Subject: [PATCH 1/6] Move root cert setup into app --- .devcontainer/devcontainer.json | 2 +- .devcontainer/docker-compose.yml | 1 + app/lib/clients/vault/certificate.rb | 62 ++++++++++++++++++------- config/astral.yml | 6 ++- lib/tasks/vault.rake | 69 ---------------------------- 5 files changed, 52 insertions(+), 88 deletions(-) delete mode 100644 lib/tasks/vault.rake diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index edddc2d..32d22d1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,7 @@ "forwardPorts": [3000, 5432, 8200], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "bundle install && rake vault:setup && rake db:setup", + "postCreateCommand": "bundle install && rake db:setup", // Configure tool-specific properties. // "customizations": {}, diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 86eb01a..dcafb8a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -14,6 +14,7 @@ services: environment: VAULT_ADDR: http://vault:8200 VAULT_TOKEN: root_token + VAULT_CREATE_ROOT: true VAULT_ROOT_CA_MOUNT: pki VAULT_ROOT_CA_REF: root-ca JWT_SIGNING_KEY: jwt_secret diff --git a/app/lib/clients/vault/certificate.rb b/app/lib/clients/vault/certificate.rb index 1aa9363..d02219b 100644 --- a/app/lib/clients/vault/certificate.rb +++ b/app/lib/clients/vault/certificate.rb @@ -9,10 +9,12 @@ def issue_cert(cert_issue_request) end def configure_pki - if enable_ca - sign_cert - configure_ca - end + # if intermediate mount exists, assume configuration is done + return if client.sys.mounts.key?(intermediate_ca_mount.to_sym) + configure_root_ca if create_root? + enable_ca + sign_cert + configure_ca end private @@ -25,6 +27,11 @@ def cert_path "#{intermediate_ca_mount}/issue/astral" end + def create_root? + create_root_config = Rails.configuration.astral[:vault_create_root] + !!ActiveModel::Type::Boolean.new.cast(create_root_config) + end + def root_ca_ref Rails.configuration.astral[:vault_root_ca_ref] end @@ -38,36 +45,59 @@ def cert_engine_type end def enable_ca - # if mount exists, assume configuration is done - if client.sys.mounts.key?(intermediate_ca_mount.to_sym) - return false - end - - # create the mount + # create the intermediate mount enable_engine(intermediate_ca_mount, cert_engine_type) - true + end + + def configure_root_ca + return if client.sys.mounts.key?(root_ca_mount.to_sym) + + # enable engine + enable_engine(root_ca_mount, cert_engine_type) + + # generate root certificate + root_cert = client.logical.write("#{root_ca_mount}/root/generate/internal", + common_name: "astral.internal", + issuer_name: root_ca_ref, + ttl: "87600h").data[:certificate] + # save the root certificate + File.write("tmp/#{root_ca_ref}.crt", root_cert) + + client.logical.write("#{root_ca_mount}/config/cluster", + path: "#{vault_address}/v1/#{root_ca_mount}", + aia_path: "#{vault_address}/v1/#{root_ca_mount}") + + client.logical.write("#{root_ca_mount}/roles/2024-servers", + allow_any_name: true, + no_store: false) + + client.logical.write("#{root_ca_mount}/config/urls", + issuing_certificates: "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", + crl_distribution_points: "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der", + ocsp_servers: "{{cluster_path}}/ocsp", + enable_templating: true) end def sign_cert - # Generate intermediate CSR + # generate intermediate CSR intermediate_csr = client.logical.write("#{intermediate_ca_mount}/intermediate/generate/internal", common_name: "astral.internal Intermediate Authority", issuer_name: "astral-intermediate").data[:csr] - # Save the intermediate CSR + # save the intermediate CSR File.write("tmp/pki_intermediate.csr", intermediate_csr) - # Sign the intermediate certificate with the root CA + # sign the intermediate certificate with the root CA intermediate_cert = client.logical.write("#{root_ca_mount}/root/sign-intermediate", issuer_ref: root_ca_ref, csr: intermediate_csr, format: "pem_bundle", ttl: "43800h").data[:certificate] - # Save the signed intermediate certificate + # save the signed intermediate certificate File.write("tmp/intermediate.cert.pem", intermediate_cert) - # Set the signed intermediate certificate + # set the signed intermediate certificate client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert) end diff --git a/config/astral.yml b/config/astral.yml index 3093e7b..f8acdeb 100644 --- a/config/astral.yml +++ b/config/astral.yml @@ -1,9 +1,11 @@ shared: vault_addr: <%= ENV["VAULT_ADDR"] %> vault_token: <%= ENV["VAULT_TOKEN"] %> - # Pre-existing root CA in Vault for signing intermediate created by astral + + # Pre-existing root CA, or create new if requested + vault_create_root: <%= ENV["VAULT_CREATE_ROOT"] || "true" %> vault_root_ca_ref: <%= ENV["VAULT_ROOT_CA_REF"] || "root-ca" %> - vault_root_ca_mount: <%= ENV["VAULT_ROOT_CA_MOUNT"] || "pki" %> + vault_root_ca_mount: <%= ENV["VAULT_ROOT_CA_MOUNT"] || "pki_root" %> jwt_signing_key: <%= ENV["JWT_SIGNING_KEY"] %> cert_ttl: <%= ENV["CERT_TTL"] %> diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake deleted file mode 100644 index ca06432..0000000 --- a/lib/tasks/vault.rake +++ /dev/null @@ -1,69 +0,0 @@ -require "rake" -require "vault" -require "json" - -# Define Rake tasks -namespace :vault do - desc "Setup PKI root certificate authority" - task :setup do - unless Rails.env.development? - raise "This task should only be used in development" - end - Vault.address = ENV["VAULT_ADDR"] - Vault.token = ENV["VAULT_TOKEN"] - ensure_root_cert - configure_root_cert - end -end - -# Helper methods -def enable_pki(path, max_ttl) - unless Vault.sys.mounts.key?(path + "/") - Vault.sys.mount(path, "pki", "PKI Secrets Engine") - else - puts "#{path} already enabled." - end -rescue Vault::HTTPError => e - puts "Error enabling pki, already enabled?: #{e}" -end - -def ensure_root_cert - enable_pki(root_mount, "87600h") - - # Generate root certificate - root_cert = Vault.logical.write("pki/root/generate/internal", - common_name: "astral.internal", - issuer_name: root_issuer_name, - ttl: "87600h").data[:certificate] - - # Save the root certificate - File.write("tmp/#{root_issuer_name}.crt", root_cert) -rescue Vault::HTTPError => e - puts "Error enabling root pki, already enabled?: #{e}" -end - -def configure_root_cert - Vault.logical.write("#{root_mount}/config/cluster", - path: "#{ENV["VAULT_ADDR"]}/v1/#{root_mount}", - aia_path: "#{ENV["VAULT_ADDR"]}/v1/#{root_mount}") - - Vault.logical.write("#{root_mount}/roles/2024-servers", - allow_any_name: true, - no_store: false) - - Vault.logical.write("#{root_mount}/config/urls", - issuing_certificates: "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", - crl_distribution_points: "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der", - ocsp_servers: "{{cluster_path}}/ocsp", - enable_templating: true) -rescue Vault::HTTPError => e - puts "Error configuring root pki: #{e}" -end - -def root_issuer_name - ENV["VAULT_ROOT_CA_REF"] -end - -def root_mount - ENV["VAULT_ROOT_CA_MOUNT"] -end From 9d656409954cede36e8b1f90602279dab32aa83c Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 30 Sep 2024 13:46:32 -0400 Subject: [PATCH 2/6] Alter test to check root and and intermediate cert creation --- test/lib/clients/vault_test.rb | 52 ++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/test/lib/clients/vault_test.rb b/test/lib/clients/vault_test.rb index ddbb38a..6fc83d0 100644 --- a/test/lib/clients/vault_test.rb +++ b/test/lib/clients/vault_test.rb @@ -1,43 +1,51 @@ require "test_helper" class VaultTest < ActiveSupport::TestCase - attr_reader :random_mount + attr_reader :intermedia_ca_mount + attr_reader :root_ca_mount setup do @client = Clients::Vault - @random_mount = SecureRandom.hex(4) + @root_ca_mount = SecureRandom.hex(4) + @intermedia_ca_mount = SecureRandom.hex(4) end teardown do - vault_client.sys.unmount(random_mount) + vault_client.sys.unmount(root_ca_mount) + vault_client.sys.unmount(intermedia_ca_mount) end test "#configure_kv" do - @client.stub :kv_mount, random_mount do + @client.stub :kv_mount, intermedia_ca_mount do assert @client.configure_kv engines = vault_client.sys.mounts - assert_equal "kv", engines[random_mount.to_sym].type + assert_equal "kv", engines[intermedia_ca_mount.to_sym].type end end test "#configure_pki" do - @client.stub :intermediate_ca_mount, random_mount do - assert @client.configure_pki - engines = vault_client.sys.mounts - assert_equal "pki", engines[random_mount.to_sym].type - - read_cert = vault_client.logical.read("#{random_mount}/cert/ca").data[:certificate] - assert_match "BEGIN CERTIFICATE", read_cert - - cluster_config = vault_client.logical.read("#{random_mount}/config/cluster").data - assert_equal "#{vault_addr}/v1/#{random_mount}", cluster_config[:path] - assert_equal "#{vault_addr}/v1/#{random_mount}", cluster_config[:aia_path] - - role_config = vault_client.logical.read("#{random_mount}/roles/astral").data - assert_not_nil role_config[:issuer_ref] - assert_equal 720.hours, role_config[:max_ttl] - assert_equal true, role_config[:allow_any_name] - end + @client.stub :root_ca_mount, root_ca_mount do + @client.stub :intermediate_ca_mount, intermedia_ca_mount do + assert @client.configure_pki + + [root_ca_mount, intermedia_ca_mount].each do |mount| + engines = vault_client.sys.mounts + assert_equal "pki", engines[mount.to_sym].type + + read_cert = vault_client.logical.read("#{mount}/cert/ca").data[:certificate] + assert_match "BEGIN CERTIFICATE", read_cert + + cluster_config = vault_client.logical.read("#{mount}/config/cluster").data + assert_equal "#{vault_addr}/v1/#{mount}", cluster_config[:path] + assert_equal "#{vault_addr}/v1/#{mount}", cluster_config[:aia_path] + end + + role_config = vault_client.logical.read("#{intermedia_ca_mount}/roles/astral").data + assert_not_nil role_config[:issuer_ref] + assert_equal 720.hours, role_config[:max_ttl] + assert_equal true, role_config[:allow_any_name] + end + end end private From 3b37d3dfdd2afee0808066f2be56753dfcc75216 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 30 Sep 2024 13:50:27 -0400 Subject: [PATCH 3/6] typo in attr name --- test/lib/clients/vault_test.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/lib/clients/vault_test.rb b/test/lib/clients/vault_test.rb index 6fc83d0..49699fc 100644 --- a/test/lib/clients/vault_test.rb +++ b/test/lib/clients/vault_test.rb @@ -1,34 +1,34 @@ require "test_helper" class VaultTest < ActiveSupport::TestCase - attr_reader :intermedia_ca_mount + attr_reader :intermediate_ca_mount attr_reader :root_ca_mount setup do @client = Clients::Vault @root_ca_mount = SecureRandom.hex(4) - @intermedia_ca_mount = SecureRandom.hex(4) + @intermediate_ca_mount = SecureRandom.hex(4) end teardown do vault_client.sys.unmount(root_ca_mount) - vault_client.sys.unmount(intermedia_ca_mount) + vault_client.sys.unmount(intermediate_ca_mount) end test "#configure_kv" do - @client.stub :kv_mount, intermedia_ca_mount do + @client.stub :kv_mount, intermediate_ca_mount do assert @client.configure_kv engines = vault_client.sys.mounts - assert_equal "kv", engines[intermedia_ca_mount.to_sym].type + assert_equal "kv", engines[intermediate_ca_mount.to_sym].type end end test "#configure_pki" do @client.stub :root_ca_mount, root_ca_mount do - @client.stub :intermediate_ca_mount, intermedia_ca_mount do + @client.stub :intermediate_ca_mount, intermediate_ca_mount do assert @client.configure_pki - [root_ca_mount, intermedia_ca_mount].each do |mount| + [root_ca_mount, intermediate_ca_mount].each do |mount| engines = vault_client.sys.mounts assert_equal "pki", engines[mount.to_sym].type @@ -40,7 +40,7 @@ class VaultTest < ActiveSupport::TestCase assert_equal "#{vault_addr}/v1/#{mount}", cluster_config[:aia_path] end - role_config = vault_client.logical.read("#{intermedia_ca_mount}/roles/astral").data + role_config = vault_client.logical.read("#{intermediate_ca_mount}/roles/astral").data assert_not_nil role_config[:issuer_ref] assert_equal 720.hours, role_config[:max_ttl] assert_equal true, role_config[:allow_any_name] From e2a3410f471cd1426fd84160ea74e952fb7ece91 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 30 Sep 2024 14:48:35 -0400 Subject: [PATCH 4/6] fix lints --- app/lib/clients/vault/certificate.rb | 12 ++++++------ test/lib/clients/vault_test.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/lib/clients/vault/certificate.rb b/app/lib/clients/vault/certificate.rb index d02219b..d5aa153 100644 --- a/app/lib/clients/vault/certificate.rb +++ b/app/lib/clients/vault/certificate.rb @@ -59,9 +59,9 @@ def configure_root_ca root_cert = client.logical.write("#{root_ca_mount}/root/generate/internal", common_name: "astral.internal", issuer_name: root_ca_ref, - ttl: "87600h").data[:certificate] + ttl: "87600h").data # save the root certificate - File.write("tmp/#{root_ca_ref}.crt", root_cert) + File.write("tmp/#{root_ca_mount}.crt", root_cert) client.logical.write("#{root_ca_mount}/config/cluster", path: "#{vault_address}/v1/#{root_ca_mount}", @@ -85,20 +85,20 @@ def sign_cert issuer_name: "astral-intermediate").data[:csr] # save the intermediate CSR - File.write("tmp/pki_intermediate.csr", intermediate_csr) + File.write("tmp/#{intermediate_ca_mount}.csr", intermediate_csr) # sign the intermediate certificate with the root CA intermediate_cert = client.logical.write("#{root_ca_mount}/root/sign-intermediate", issuer_ref: root_ca_ref, csr: intermediate_csr, format: "pem_bundle", - ttl: "43800h").data[:certificate] + ttl: "43800h").data # save the signed intermediate certificate - File.write("tmp/intermediate.cert.pem", intermediate_cert) + File.write("tmp/#{intermediate_ca_mount}.cert.pem", intermediate_cert) # set the signed intermediate certificate - client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert) + client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert[:certificate]) end def configure_ca diff --git a/test/lib/clients/vault_test.rb b/test/lib/clients/vault_test.rb index 49699fc..23f194a 100644 --- a/test/lib/clients/vault_test.rb +++ b/test/lib/clients/vault_test.rb @@ -28,7 +28,7 @@ class VaultTest < ActiveSupport::TestCase @client.stub :intermediate_ca_mount, intermediate_ca_mount do assert @client.configure_pki - [root_ca_mount, intermediate_ca_mount].each do |mount| + [ root_ca_mount, intermediate_ca_mount ].each do |mount| engines = vault_client.sys.mounts assert_equal "pki", engines[mount.to_sym].type From 7a73bdc77d08ffab132c1ecd6807edb2e04215a2 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 30 Sep 2024 15:42:42 -0400 Subject: [PATCH 5/6] Fix flakey test and deprecation warning --- Gemfile | 1 + Gemfile.lock | 2 ++ app/lib/clients/vault/certificate.rb | 6 +++--- test/integration/secrets_test.rb | 13 ++++++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index abcd601..126add0 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem "bootsnap", require: false # High-level app logic gem "interactor", "~> 3.0" +gem "ostruct" # Use the vault-ruby gem to interact with HashiCorp Vault gem "vault" diff --git a/Gemfile.lock b/Gemfile.lock index f416225..2f10853 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,6 +152,7 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + ostruct (0.6.0) parallel (1.26.2) parser (3.3.4.2) ast (~> 2.4.1) @@ -290,6 +291,7 @@ DEPENDENCIES interactor (~> 3.0) jbuilder jwt + ostruct puma (>= 5.0) rails (~> 7.2.1) rubocop-rails-omakase diff --git a/app/lib/clients/vault/certificate.rb b/app/lib/clients/vault/certificate.rb index d5aa153..2ac8764 100644 --- a/app/lib/clients/vault/certificate.rb +++ b/app/lib/clients/vault/certificate.rb @@ -59,7 +59,7 @@ def configure_root_ca root_cert = client.logical.write("#{root_ca_mount}/root/generate/internal", common_name: "astral.internal", issuer_name: root_ca_ref, - ttl: "87600h").data + ttl: "87600h").data[:certificate] # save the root certificate File.write("tmp/#{root_ca_mount}.crt", root_cert) @@ -92,13 +92,13 @@ def sign_cert issuer_ref: root_ca_ref, csr: intermediate_csr, format: "pem_bundle", - ttl: "43800h").data + ttl: "43800h").data[:certificate] # save the signed intermediate certificate File.write("tmp/#{intermediate_ca_mount}.cert.pem", intermediate_cert) # set the signed intermediate certificate - client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert[:certificate]) + client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert) end def configure_ca diff --git a/test/integration/secrets_test.rb b/test/integration/secrets_test.rb index 54f56b0..bdb1ac8 100644 --- a/test/integration/secrets_test.rb +++ b/test/integration/secrets_test.rb @@ -20,9 +20,9 @@ class SecretsTest < ActionDispatch::IntegrationTest end test "#show" do - create_secret + path = create_secret # view the secret - get secret_path("top/secret/key"), headers: { "Authorization" => "Bearer #{jwt_authorized}" } + get secret_path(path), headers: { "Authorization" => "Bearer #{jwt_authorized}" } assert_response :success %w[ data metadata lease_id ].each do |key| assert_includes response.parsed_body["secret"].keys, key @@ -30,18 +30,21 @@ class SecretsTest < ActionDispatch::IntegrationTest end test "#delete" do - create_secret + path = create_secret # delete the secret - delete destroy_secret_path("top/secret/key"), headers: { "Authorization" => "Bearer #{jwt_authorized}" } + delete destroy_secret_path(path), headers: { "Authorization" => "Bearer #{jwt_authorized}" } assert_response :success end private def create_secret + # make a path + path = "top/secret/#{SecureRandom.hex}" # create the secret post secrets_path, headers: { "Authorization" => "Bearer #{jwt_authorized}" }, - params: { secret: { path: "top/secret/key", data: { password: "sicr3t" } } } + params: { secret: { path: path, data: { password: "sicr3t" } } } + path end def remove_pki_engine From 5ed355a430c2c37750b0ab27b826d28f6da3f5b5 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 1 Oct 2024 16:01:04 -0400 Subject: [PATCH 6/6] removed the root-cert role until we find a need --- app/lib/clients/vault/certificate.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/lib/clients/vault/certificate.rb b/app/lib/clients/vault/certificate.rb index 2ac8764..bd83cd6 100644 --- a/app/lib/clients/vault/certificate.rb +++ b/app/lib/clients/vault/certificate.rb @@ -67,10 +67,6 @@ def configure_root_ca path: "#{vault_address}/v1/#{root_ca_mount}", aia_path: "#{vault_address}/v1/#{root_ca_mount}") - client.logical.write("#{root_ca_mount}/roles/2024-servers", - allow_any_name: true, - no_store: false) - client.logical.write("#{root_ca_mount}/config/urls", issuing_certificates: "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", crl_distribution_points: "{{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der", @@ -95,7 +91,7 @@ def sign_cert ttl: "43800h").data[:certificate] # save the signed intermediate certificate - File.write("tmp/#{intermediate_ca_mount}.cert.pem", intermediate_cert) + File.write("tmp/#{intermediate_ca_mount}.crt", intermediate_cert) # set the signed intermediate certificate client.logical.write("#{intermediate_ca_mount}/intermediate/set-signed", certificate: intermediate_cert)