From a22dc3978044e60485143dea3041598957f0d6ed Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 12 Aug 2024 15:08:04 -0400 Subject: [PATCH 01/33] First bits to integrate with vault server in devcontainer environment --- .devcontainer/devcontainer.json | 4 +- .devcontainer/docker-compose.yml | 43 +++----- app/controllers/application_controller.rb | 7 ++ app/controllers/certificates_controller.rb | 10 ++ config/routes.rb | 4 +- lib/services/vault_service.rb | 30 ++++++ lib/tasks/vault.rake | 102 ++++++++++++++++++ .../certificates_controller_test.rb | 7 ++ 8 files changed, 174 insertions(+), 33 deletions(-) create mode 100644 app/controllers/certificates_controller.rb create mode 100644 lib/services/vault_service.rb create mode 100644 lib/tasks/vault.rake create mode 100644 test/controllers/certificates_controller_test.rb diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b330956..c7e487c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "name": "Astral-Rails", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -14,7 +14,7 @@ // "forwardPorts": [3000, 5432, 8200] // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "bundle install && rake db:setup", + "postCreateCommand": "bundle install && rake vault:setup" // Configure tool-specific properties. // "customizations": {}, diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index c435755..272a0f1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -13,7 +13,9 @@ services: command: sleep infinity # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - network_mode: service:db + networks: + astral: + ipv4_address: "10.1.10.200" environment: VAULT_ADDRESS: http://vault:8200 @@ -25,32 +27,13 @@ services: environment: VAULT_DEV_ROOT_TOKEN_ID: root_token VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 - - db: - image: postgres:latest - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql/data - - ./create-db-user.sql:/docker-entrypoint-initdb.d/create-db-user.sql - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - # Your config/database.yml should use the user and password you set here, - # and host "db" (as that's the name of this service). You can use whatever - # database name you want. Use `bin/rails db:prepare` to create the database. - # - # Example: - # - # development: - # <<: *default - # host: db - # username: postgres - # password: postgres - # database: myapp_development - - # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - -volumes: - postgres-data: \ No newline at end of file + networks: + astral: + ipv4_address: "10.1.10.100" + +networks: + astral: + ipam: + driver: default + config: + - subnet: "10.1.10.0/24" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ac8823..2268f18 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,9 @@ class ApplicationController < ActionController::API + def info + render json: { + app: "astral", + description: "Astral provides a simplified API for app secrets.", + version: "0.0.1" + } + end end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb new file mode 100644 index 0000000..2a844c1 --- /dev/null +++ b/app/controllers/certificates_controller.rb @@ -0,0 +1,10 @@ +class CertificatesController < ApplicationController + def index + name = params[:common_name] || "host.example.com" + ttl = params[:ttl] || "24h" + cert = Services::VaultService.new.new_cert(name, ttl) + render json: { + cert: cert + } + end +end diff --git a/config/routes.rb b/config/routes.rb index 33c9639..746c547 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,5 +10,7 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # Defines the root path route ("/") - # root "posts#index" + root "application#info" + + resources :certificates, only: %i[index] end diff --git a/lib/services/vault_service.rb b/lib/services/vault_service.rb new file mode 100644 index 0000000..0f0e754 --- /dev/null +++ b/lib/services/vault_service.rb @@ -0,0 +1,30 @@ +module Services + class VaultService + def initialize + @client = Vault::Client.new( + address: ENV["VAULT_ADDR"], + token: ENV["VAULT_TOKEN"] + ) + end + + def new_cert(common_name, ttl) + # Generate the mTLS certificate using the intermediate CA + mtls_cert = Vault.logical.write("pki-intermediate/issue/client-cert", + common_name: common_name, + ttl: ttl, + ip_sans: "192.168.1.1", + format: "pem") + + # Extract the certificate, private key, and issuing CA chain + certificate = mtls_cert.data["certificate"] + private_key = mtls_cert.data["private_key"] + issuing_ca = mtls_cert.data["issuing_ca"] + + # Print the certificate details + puts "Certificate:\n#{certificate}" + puts "Private Key:\n#{private_key}" + puts "Issuing CA Chain:\n#{issuing_ca}" + mtls_cert + end + end +end diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake new file mode 100644 index 0000000..0dff9d8 --- /dev/null +++ b/lib/tasks/vault.rake @@ -0,0 +1,102 @@ +require "rake" +require "vault" +require "json" + +# Helper method to enable PKI engine for dev environment +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 +end + +# Define Rake tasks +namespace :vault do + desc "Setup PKI root and intermediate certificates" + task :setup do + Vault.address = ENV["VAULT_ADDRESS"] + Vault.token = ENV["VAULT_TOKEN"] + Rake::Task["vault:enable_root_pki"].invoke + Rake::Task["vault:configure_root_pki"].invoke + Rake::Task["vault:enable_intermediate_pki"].invoke + Rake::Task["vault:configure_intermediate_pki"].invoke + end + + desc "Enable and configure root PKI" + task :enable_root_pki do + enable_pki("pki", "87600h") + + # Generate root certificate + root_cert = Vault.logical.write("pki/root/generate/internal", + common_name: "astral.internal", + issuer_name: "root-2024", + ttl: "87600h").data["certificate"] + + # Save the root certificate + File.write("root_2024_ca.crt", root_cert) + end + + desc "Configure root PKI" + task :configure_root_pki do + Vault.logical.write("pki/config/cluster", + path: "http://10.1.10.100:8200/v1/pki", + aia_path: "http://10.1.10.100:8200/v1/pki") + + Vault.logical.write("pki/roles/2023-servers", + allow_any_name: true, + no_store: false) + + Vault.logical.write("pki/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 + + desc "Enable and configure intermediate PKI" + task :enable_intermediate_pki do + enable_pki("pki_int", "43800h") + + # Generate intermediate CSR + intermediate_csr = Vault.logical.write("pki_int/intermediate/generate/internal", + common_name: "astral.internal Intermediate Authority", + issuer_name: "learn-intermediate").data["csr"] + + # Save the intermediate CSR + File.write("pki_intermediate.csr", intermediate_csr) + + # Sign the intermediate certificate with the root CA + intermediate_cert = Vault.logical.write("pki/root/sign-intermediate", + issuer_ref: "root-2024", + csr: intermediate_csr, + format: "pem_bundle", + ttl: "43800h").data["certificate"] + + # Save the signed intermediate certificate + File.write("intermediate.cert.pem", intermediate_cert) + + # Set the signed intermediate certificate + Vault.logical.write("pki_int/intermediate/set-signed", certificate: intermediate_cert) + end + + desc "Configure intermediate PKI" + task :configure_intermediate_pki do + Vault.logical.write("pki_int/config/cluster", + path: "http://10.1.10.100:8200/v1/pki_int", + aia_path: "http://10.1.10.100:8200/v1/pki_int") + + issuer_ref = Vault.logical.read("pki_int/config/issuers").data["default"] + Vault.logical.write("pki_int/roles/learn", + issuer_ref: issuer_ref, + allow_any_name: true, + max_ttl: "720h", + no_store: false) + + Vault.logical.write("pki_int/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 +end diff --git a/test/controllers/certificates_controller_test.rb b/test/controllers/certificates_controller_test.rb new file mode 100644 index 0000000..24f3196 --- /dev/null +++ b/test/controllers/certificates_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class CertificatesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end From acbf2136f6b86cb8cea3e065accf07997fde6ef0 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 13 Aug 2024 16:01:13 -0400 Subject: [PATCH 02/33] Issuing cert from vault at /certificates endpoint --- .devcontainer/docker-compose.yml | 2 +- app/controllers/application_controller.rb | 2 +- app/controllers/certificates_controller.rb | 4 +--- lib/services/vault_service.rb | 17 +++-------------- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 272a0f1..9f2a2b4 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -18,7 +18,7 @@ services: ipv4_address: "10.1.10.200" environment: - VAULT_ADDRESS: http://vault:8200 + VAULT_ADDR: http://vault:8200 VAULT_TOKEN: root_token vault: diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2268f18..af4c5dd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ class ApplicationController < ActionController::API def info render json: { app: "astral", - description: "Astral provides a simplified API for app secrets.", + description: "Astral provides a simplified API for PKI.", version: "0.0.1" } end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 2a844c1..5e151ba 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -3,8 +3,6 @@ def index name = params[:common_name] || "host.example.com" ttl = params[:ttl] || "24h" cert = Services::VaultService.new.new_cert(name, ttl) - render json: { - cert: cert - } + render json: cert end end diff --git a/lib/services/vault_service.rb b/lib/services/vault_service.rb index 0f0e754..69f804a 100644 --- a/lib/services/vault_service.rb +++ b/lib/services/vault_service.rb @@ -8,23 +8,12 @@ def initialize end def new_cert(common_name, ttl) - # Generate the mTLS certificate using the intermediate CA - mtls_cert = Vault.logical.write("pki-intermediate/issue/client-cert", - common_name: common_name, + # Generate the TLS certificate using the intermediate CA + tls_cert = @client.logical.write("pki_int/issue/learn", common_name: common_name, ttl: ttl, ip_sans: "192.168.1.1", format: "pem") - - # Extract the certificate, private key, and issuing CA chain - certificate = mtls_cert.data["certificate"] - private_key = mtls_cert.data["private_key"] - issuing_ca = mtls_cert.data["issuing_ca"] - - # Print the certificate details - puts "Certificate:\n#{certificate}" - puts "Private Key:\n#{private_key}" - puts "Issuing CA Chain:\n#{issuing_ca}" - mtls_cert + tls_cert.data end end end From 06958fcbc224f4bafc871468dadf653dc2383437 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 13 Aug 2024 16:01:37 -0400 Subject: [PATCH 03/33] Add error rescues to rake task vault:setup --- lib/tasks/vault.rake | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake index 0dff9d8..8a82e20 100644 --- a/lib/tasks/vault.rake +++ b/lib/tasks/vault.rake @@ -9,13 +9,15 @@ def enable_pki(path, max_ttl) else puts "#{path} already enabled." end +rescue Vault::HTTPError => e + puts "Error enabling pki, already enabled?: #{e}" end # Define Rake tasks namespace :vault do desc "Setup PKI root and intermediate certificates" task :setup do - Vault.address = ENV["VAULT_ADDRESS"] + Vault.address = ENV["VAULT_ADDR"] Vault.token = ENV["VAULT_TOKEN"] Rake::Task["vault:enable_root_pki"].invoke Rake::Task["vault:configure_root_pki"].invoke @@ -31,10 +33,12 @@ namespace :vault do root_cert = Vault.logical.write("pki/root/generate/internal", common_name: "astral.internal", issuer_name: "root-2024", - ttl: "87600h").data["certificate"] + ttl: "87600h").data[:certificate] # Save the root certificate File.write("root_2024_ca.crt", root_cert) + rescue Vault::HTTPError => e + puts "Error enabling root pki, already enabled?: #{e}" end desc "Configure root PKI" @@ -43,7 +47,7 @@ namespace :vault do path: "http://10.1.10.100:8200/v1/pki", aia_path: "http://10.1.10.100:8200/v1/pki") - Vault.logical.write("pki/roles/2023-servers", + Vault.logical.write("pki/roles/2024-servers", allow_any_name: true, no_store: false) @@ -52,6 +56,8 @@ namespace :vault do 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, already enabled?: #{e}" end desc "Enable and configure intermediate PKI" @@ -61,7 +67,7 @@ namespace :vault do # Generate intermediate CSR intermediate_csr = Vault.logical.write("pki_int/intermediate/generate/internal", common_name: "astral.internal Intermediate Authority", - issuer_name: "learn-intermediate").data["csr"] + issuer_name: "learn-intermediate").data[:csr] # Save the intermediate CSR File.write("pki_intermediate.csr", intermediate_csr) @@ -71,13 +77,15 @@ namespace :vault do issuer_ref: "root-2024", csr: intermediate_csr, format: "pem_bundle", - ttl: "43800h").data["certificate"] + ttl: "43800h").data[:certificate] # Save the signed intermediate certificate File.write("intermediate.cert.pem", intermediate_cert) # Set the signed intermediate certificate Vault.logical.write("pki_int/intermediate/set-signed", certificate: intermediate_cert) + rescue Vault::HTTPError => e + puts "Error enabling intermediate pki, already enabled?: #{e}" end desc "Configure intermediate PKI" @@ -86,7 +94,7 @@ namespace :vault do path: "http://10.1.10.100:8200/v1/pki_int", aia_path: "http://10.1.10.100:8200/v1/pki_int") - issuer_ref = Vault.logical.read("pki_int/config/issuers").data["default"] + issuer_ref = Vault.logical.read("pki_int/config/issuers").data[:default] Vault.logical.write("pki_int/roles/learn", issuer_ref: issuer_ref, allow_any_name: true, @@ -98,5 +106,7 @@ namespace :vault do 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 intermediate pki, already enabled?: #{e}" end end From f9c24539a34a5a26675213297a7e69a008c95567 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 14 Aug 2024 14:04:19 -0400 Subject: [PATCH 04/33] Update readme; expose ports; code format --- .devcontainer/devcontainer.json | 6 +++--- README.md | 29 +++++++++++------------------ lib/services/vault_service.rb | 9 +++++---- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c7e487c..9b99382 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,14 +11,14 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. // This can be used to network with other containers or the host. - // "forwardPorts": [3000, 5432, 8200] + "forwardPorts": [3000, 5432, 8200], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "bundle install && rake vault:setup" + "postCreateCommand": "bundle install && rake vault:setup", // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "remoteUser": "vscode" } diff --git a/README.md b/README.md index 7db80e4..2a28245 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,17 @@ # README -This README would normally document whatever steps are necessary to get the -application up and running. +Astral-rails is a proof-of-concept api application intended to simplify +certificate acquisition for other applications/services. Broadly speaking, +it will: -Things you may want to cover: +1) Authorize the request for cerficate using a third party trusted source (JWT, etc) +2) If authorized, obtain a certificate from PKI CLM (such as Vault/OpenBao) +3) Log this transaction in audit infrastructure (ELK, etc). -* Ruby version +# Running -* System dependencies +This app is most easily run and developed in its devcontainer. -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... +1) Open in devcontainer +2) Launch server +3) POST /certificates to acquire cert diff --git a/lib/services/vault_service.rb b/lib/services/vault_service.rb index 69f804a..02ac609 100644 --- a/lib/services/vault_service.rb +++ b/lib/services/vault_service.rb @@ -9,10 +9,11 @@ def initialize def new_cert(common_name, ttl) # Generate the TLS certificate using the intermediate CA - tls_cert = @client.logical.write("pki_int/issue/learn", common_name: common_name, - ttl: ttl, - ip_sans: "192.168.1.1", - format: "pem") + tls_cert = @client.logical.write("pki_int/issue/learn", + common_name: common_name, + ttl: ttl, + ip_sans: "192.168.1.1", + format: "pem") tls_cert.data end end From ce2e1ef9e1e31b0fedbf5f70480cf2f09e1c4f22 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 15 Aug 2024 14:58:09 -0400 Subject: [PATCH 05/33] Fix rake task and Gemfile --- Gemfile | 4 +- lib/tasks/vault.rake | 171 +++++++++++++++++++++---------------------- 2 files changed, 87 insertions(+), 88 deletions(-) diff --git a/Gemfile b/Gemfile index e1a08a9..3bb300d 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem "puma", ">= 5.0" # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ windows jruby ] +gem "tzinfo-data", platforms: %i[ mswin jruby ] # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false @@ -34,7 +34,7 @@ gem "vault" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + gem "debug", platforms: %i[ mri mswin ], require: "debug/prelude" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem "brakeman", require: false diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake index 8a82e20..33621dd 100644 --- a/lib/tasks/vault.rake +++ b/lib/tasks/vault.rake @@ -2,7 +2,23 @@ require "rake" require "vault" require "json" -# Helper method to enable PKI engine for dev environment +# Define Rake tasks +namespace :vault do + desc "Setup PKI root and intermediate certificates" + 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"] + enable_root_pki + configure_root_pki + enable_intermediate_pki + configure_intermediate_pki + end +end + +# Helper methods def enable_pki(path, max_ttl) unless Vault.sys.mounts.key?(path + "/") Vault.sys.mount(path, "pki", "PKI Secrets Engine") @@ -13,100 +29,83 @@ rescue Vault::HTTPError => e puts "Error enabling pki, already enabled?: #{e}" end -# Define Rake tasks -namespace :vault do - desc "Setup PKI root and intermediate certificates" - task :setup do - Vault.address = ENV["VAULT_ADDR"] - Vault.token = ENV["VAULT_TOKEN"] - Rake::Task["vault:enable_root_pki"].invoke - Rake::Task["vault:configure_root_pki"].invoke - Rake::Task["vault:enable_intermediate_pki"].invoke - Rake::Task["vault:configure_intermediate_pki"].invoke - end +def enable_root_pki + enable_pki("pki", "87600h") - desc "Enable and configure root PKI" - task :enable_root_pki do - enable_pki("pki", "87600h") + # Generate root certificate + root_cert = Vault.logical.write("pki/root/generate/internal", + common_name: "astral.internal", + issuer_name: "root-2024", + ttl: "87600h").data[:certificate] - # Generate root certificate - root_cert = Vault.logical.write("pki/root/generate/internal", - common_name: "astral.internal", - issuer_name: "root-2024", - ttl: "87600h").data[:certificate] + # Save the root certificate + File.write("tmp/root_2024_ca.crt", root_cert) +rescue Vault::HTTPError => e + puts "Error enabling root pki, already enabled?: #{e}" +end - # Save the root certificate - File.write("root_2024_ca.crt", root_cert) - rescue Vault::HTTPError => e - puts "Error enabling root pki, already enabled?: #{e}" - end +def configure_root_pki + Vault.logical.write("pki/config/cluster", + path: "http://10.1.10.100:8200/v1/pki", + aia_path: "http://10.1.10.100:8200/v1/pki") - desc "Configure root PKI" - task :configure_root_pki do - Vault.logical.write("pki/config/cluster", - path: "http://10.1.10.100:8200/v1/pki", - aia_path: "http://10.1.10.100:8200/v1/pki") - - Vault.logical.write("pki/roles/2024-servers", - allow_any_name: true, - no_store: false) - - Vault.logical.write("pki/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, already enabled?: #{e}" - end + Vault.logical.write("pki/roles/2024-servers", + allow_any_name: true, + no_store: false) + + Vault.logical.write("pki/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, already enabled?: #{e}" +end - desc "Enable and configure intermediate PKI" - task :enable_intermediate_pki do - enable_pki("pki_int", "43800h") +def enable_intermediate_pki + enable_pki("pki_int", "43800h") - # Generate intermediate CSR - intermediate_csr = Vault.logical.write("pki_int/intermediate/generate/internal", - common_name: "astral.internal Intermediate Authority", - issuer_name: "learn-intermediate").data[:csr] + # Generate intermediate CSR + intermediate_csr = Vault.logical.write("pki_int/intermediate/generate/internal", + common_name: "astral.internal Intermediate Authority", + issuer_name: "learn-intermediate").data[:csr] - # Save the intermediate CSR - File.write("pki_intermediate.csr", intermediate_csr) + # Save the intermediate CSR + File.write("tmp/pki_intermediate.csr", intermediate_csr) - # Sign the intermediate certificate with the root CA - intermediate_cert = Vault.logical.write("pki/root/sign-intermediate", - issuer_ref: "root-2024", - csr: intermediate_csr, - format: "pem_bundle", - ttl: "43800h").data[:certificate] + # Sign the intermediate certificate with the root CA + intermediate_cert = Vault.logical.write("pki/root/sign-intermediate", + issuer_ref: "root-2024", + csr: intermediate_csr, + format: "pem_bundle", + ttl: "43800h").data[:certificate] - # Save the signed intermediate certificate - File.write("intermediate.cert.pem", intermediate_cert) + # Save the signed intermediate certificate + File.write("tmp/intermediate.cert.pem", intermediate_cert) - # Set the signed intermediate certificate - Vault.logical.write("pki_int/intermediate/set-signed", certificate: intermediate_cert) - rescue Vault::HTTPError => e - puts "Error enabling intermediate pki, already enabled?: #{e}" - end + # Set the signed intermediate certificate + Vault.logical.write("pki_int/intermediate/set-signed", certificate: intermediate_cert) +rescue Vault::HTTPError => e + puts "Error enabling intermediate pki, already enabled?: #{e}" +end - desc "Configure intermediate PKI" - task :configure_intermediate_pki do - Vault.logical.write("pki_int/config/cluster", - path: "http://10.1.10.100:8200/v1/pki_int", - aia_path: "http://10.1.10.100:8200/v1/pki_int") - - issuer_ref = Vault.logical.read("pki_int/config/issuers").data[:default] - Vault.logical.write("pki_int/roles/learn", - issuer_ref: issuer_ref, - allow_any_name: true, - max_ttl: "720h", - no_store: false) - - Vault.logical.write("pki_int/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 intermediate pki, already enabled?: #{e}" - end +def configure_intermediate_pki + Vault.logical.write("pki_int/config/cluster", + path: "http://10.1.10.100:8200/v1/pki_int", + aia_path: "http://10.1.10.100:8200/v1/pki_int") + + issuer_ref = Vault.logical.read("pki_int/config/issuers").data[:default] + Vault.logical.write("pki_int/roles/learn", + issuer_ref: issuer_ref, + allow_any_name: true, + max_ttl: "720h", + no_store: false) + + Vault.logical.write("pki_int/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 intermediate pki, already enabled?: #{e}" end From 4d54489f949a519690cf88babf71daf094d28b97 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 15 Aug 2024 16:01:31 -0400 Subject: [PATCH 06/33] A bit of renaming/var reuse --- .devcontainer/docker-compose.yml | 2 +- lib/tasks/vault.rake | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 9f2a2b4..3e27d32 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -18,7 +18,7 @@ services: ipv4_address: "10.1.10.200" environment: - VAULT_ADDR: http://vault:8200 + VAULT_ADDR: http://10.1.10.100:8200 VAULT_TOKEN: root_token vault: diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake index 33621dd..7ef3a4e 100644 --- a/lib/tasks/vault.rake +++ b/lib/tasks/vault.rake @@ -11,10 +11,10 @@ namespace :vault do end Vault.address = ENV["VAULT_ADDR"] Vault.token = ENV["VAULT_TOKEN"] - enable_root_pki - configure_root_pki - enable_intermediate_pki - configure_intermediate_pki + ensure_root_cert + configure_root_cert + ensure_intermediate_cert + configure_intermediate_cert end end @@ -29,7 +29,7 @@ rescue Vault::HTTPError => e puts "Error enabling pki, already enabled?: #{e}" end -def enable_root_pki +def ensure_root_cert enable_pki("pki", "87600h") # Generate root certificate @@ -44,10 +44,10 @@ rescue Vault::HTTPError => e puts "Error enabling root pki, already enabled?: #{e}" end -def configure_root_pki +def configure_root_cert Vault.logical.write("pki/config/cluster", - path: "http://10.1.10.100:8200/v1/pki", - aia_path: "http://10.1.10.100:8200/v1/pki") + path: "#{ENV["VAULT_ADDR"]}/v1/pki", + aia_path: "#{ENV["VAULT_ADDR"]}/v1/pki") Vault.logical.write("pki/roles/2024-servers", allow_any_name: true, @@ -62,7 +62,7 @@ rescue Vault::HTTPError => e puts "Error configuring root pki, already enabled?: #{e}" end -def enable_intermediate_pki +def ensure_intermediate_cert enable_pki("pki_int", "43800h") # Generate intermediate CSR @@ -89,10 +89,10 @@ rescue Vault::HTTPError => e puts "Error enabling intermediate pki, already enabled?: #{e}" end -def configure_intermediate_pki +def configure_intermediate_cert Vault.logical.write("pki_int/config/cluster", - path: "http://10.1.10.100:8200/v1/pki_int", - aia_path: "http://10.1.10.100:8200/v1/pki_int") + path: "#{ENV["VAULT_ADDR"]}/v1/pki_int", + aia_path: "#{ENV["VAULT_ADDR"]}/v1/pki_int") issuer_ref = Vault.logical.read("pki_int/config/issuers").data[:default] Vault.logical.write("pki_int/roles/learn", From a536ef4564a66eacb060a601f0064c32e9886bea Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 15 Aug 2024 16:32:25 -0400 Subject: [PATCH 07/33] Added a simple integration test --- app/controllers/certificates_controller.rb | 2 +- config/routes.rb | 2 +- lib/tasks/vault.rake | 4 ++-- test/controllers/certificates_controller_test.rb | 9 ++++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 5e151ba..256bbd0 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -1,5 +1,5 @@ class CertificatesController < ApplicationController - def index + def create name = params[:common_name] || "host.example.com" ttl = params[:ttl] || "24h" cert = Services::VaultService.new.new_cert(name, ttl) diff --git a/config/routes.rb b/config/routes.rb index 746c547..0de2bf3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,5 +12,5 @@ # Defines the root path route ("/") root "application#info" - resources :certificates, only: %i[index] + resources :certificates, only: %i[create] end diff --git a/lib/tasks/vault.rake b/lib/tasks/vault.rake index 7ef3a4e..ca1bc97 100644 --- a/lib/tasks/vault.rake +++ b/lib/tasks/vault.rake @@ -91,8 +91,8 @@ end def configure_intermediate_cert Vault.logical.write("pki_int/config/cluster", - path: "#{ENV["VAULT_ADDR"]}/v1/pki_int", - aia_path: "#{ENV["VAULT_ADDR"]}/v1/pki_int") + path: "#{Vault.address}/v1/pki_int", + aia_path: "#{Vault.address}/v1/pki_int") issuer_ref = Vault.logical.read("pki_int/config/issuers").data[:default] Vault.logical.write("pki_int/roles/learn", diff --git a/test/controllers/certificates_controller_test.rb b/test/controllers/certificates_controller_test.rb index 24f3196..6f704f8 100644 --- a/test/controllers/certificates_controller_test.rb +++ b/test/controllers/certificates_controller_test.rb @@ -1,7 +1,10 @@ require "test_helper" class CertificatesControllerTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end + test "create" do + post "/certificates" + assert_response :success + assert_includes response.parsed_body.keys, "ca_chain" + assert_includes response.parsed_body.keys, "certificate" + end end From c7993b5b49bbcb7ec113d3c2ffb8bc05a1bc677d Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Fri, 16 Aug 2024 11:16:16 -0400 Subject: [PATCH 08/33] abstract the certificate creation service --- app/controllers/certificates_controller.rb | 2 +- lib/services/certificate_service.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 lib/services/certificate_service.rb diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 256bbd0..ba68c26 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -2,7 +2,7 @@ class CertificatesController < ApplicationController def create name = params[:common_name] || "host.example.com" ttl = params[:ttl] || "24h" - cert = Services::VaultService.new.new_cert(name, ttl) + cert = Services::CertificateService.new.new_cert(name, ttl) render json: cert end end diff --git a/lib/services/certificate_service.rb b/lib/services/certificate_service.rb new file mode 100644 index 0000000..42ef902 --- /dev/null +++ b/lib/services/certificate_service.rb @@ -0,0 +1,12 @@ +module Services + class CertificateService + def initialize + # this should select an implementation service based on config + @impl = VaultService.new + end + + def new_cert(common_name, ttl) + @impl.new_cert(common_name, ttl) + end + end +end From f8f909d412b81a271c8a7bfeedf975dfa6240dbc Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Fri, 16 Aug 2024 13:27:46 -0400 Subject: [PATCH 09/33] simplify integration test --- test/controllers/certificates_controller_test.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/controllers/certificates_controller_test.rb b/test/controllers/certificates_controller_test.rb index 6f704f8..9811a14 100644 --- a/test/controllers/certificates_controller_test.rb +++ b/test/controllers/certificates_controller_test.rb @@ -4,7 +4,14 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest test "create" do post "/certificates" assert_response :success - assert_includes response.parsed_body.keys, "ca_chain" - assert_includes response.parsed_body.keys, "certificate" + %w[ ca_chain + certificate + expiration + issuing_ca + private_key + private_key_type + serial_number ].each do |key| + assert_includes response.parsed_body.keys, key + end end end From f7ebb9230181c4fff0d99e220faf11fe1a3d54c2 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Fri, 16 Aug 2024 13:41:26 -0400 Subject: [PATCH 10/33] quick rename --- app/controllers/certificates_controller.rb | 2 +- lib/services/certificate_service.rb | 4 ++-- lib/services/vault_service.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index ba68c26..7a18e8a 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -2,7 +2,7 @@ class CertificatesController < ApplicationController def create name = params[:common_name] || "host.example.com" ttl = params[:ttl] || "24h" - cert = Services::CertificateService.new.new_cert(name, ttl) + cert = Services::CertificateService.new.get_cert(name, ttl) render json: cert end end diff --git a/lib/services/certificate_service.rb b/lib/services/certificate_service.rb index 42ef902..a8636b5 100644 --- a/lib/services/certificate_service.rb +++ b/lib/services/certificate_service.rb @@ -5,8 +5,8 @@ def initialize @impl = VaultService.new end - def new_cert(common_name, ttl) - @impl.new_cert(common_name, ttl) + def get_cert(common_name, ttl) + @impl.get_cert(common_name, ttl) end end end diff --git a/lib/services/vault_service.rb b/lib/services/vault_service.rb index 02ac609..5cf2881 100644 --- a/lib/services/vault_service.rb +++ b/lib/services/vault_service.rb @@ -7,7 +7,7 @@ def initialize ) end - def new_cert(common_name, ttl) + def get_cert(common_name, ttl) # Generate the TLS certificate using the intermediate CA tls_cert = @client.logical.write("pki_int/issue/learn", common_name: common_name, From b947ddd0f152687dcfb681a2f577eb7bef42c27e Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Fri, 16 Aug 2024 16:59:45 -0400 Subject: [PATCH 11/33] WIP to add oidc/jwt auth service (AppRegistry?) --- Gemfile | 3 +++ Gemfile.lock | 3 +++ lib/services/auth_service.rb | 11 +++++++++++ 3 files changed, 17 insertions(+) create mode 100644 lib/services/auth_service.rb diff --git a/Gemfile b/Gemfile index 3bb300d..a8e16e9 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,9 @@ gem "bootsnap", require: false # Use the vault-ruby gem to interact with HashiCorp Vault gem "vault" +# Use the jwt gem to decode access tokens +gem "jwt" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index 7fca954..fef8de0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) + jwt (2.8.2) + base64 language_server-protocol (3.17.0.3) logger (1.6.0) loofah (2.22.0) @@ -268,6 +270,7 @@ DEPENDENCIES bootsnap brakeman debug + jwt puma (>= 5.0) rails (~> 7.2.0) rubocop-rails-omakase diff --git a/lib/services/auth_service.rb b/lib/services/auth_service.rb new file mode 100644 index 0000000..d51bff8 --- /dev/null +++ b/lib/services/auth_service.rb @@ -0,0 +1,11 @@ +module Services + class AuthService + def decode(jwt_token) + # Decode a JWT access token using the configured base. + body = JWT.decode(token, ENV["JWT_TOKEN_BASE"])[0] + HashWithIndifferentAccess.new body + rescue + nil + end + end +end From 3771e3a6369536d12feb0130c4ca2aa59d43fe51 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Fri, 16 Aug 2024 18:07:40 -0400 Subject: [PATCH 12/33] move app code from lib -> app/lib; get an set up authentication pathway --- app/controllers/application_controller.rb | 19 +++++++++++++++++++ app/controllers/certificates_controller.rb | 2 ++ app/lib/auth_error.rb | 3 +++ {lib => app/lib}/services/auth_service.rb | 7 +++++++ .../lib}/services/certificate_service.rb | 0 {lib => app/lib}/services/vault_service.rb | 0 6 files changed, 31 insertions(+) create mode 100644 app/lib/auth_error.rb rename {lib => app/lib}/services/auth_service.rb (61%) rename {lib => app/lib}/services/certificate_service.rb (100%) rename {lib => app/lib}/services/vault_service.rb (100%) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index af4c5dd..686971b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,7 @@ class ApplicationController < ActionController::API + rescue_from StandardError, with: :handle_standard_error + rescue_from AuthError, with: :handle_auth_error + def info render json: { app: "astral", @@ -6,4 +9,20 @@ def info version: "0.0.1" } end + + def authenticate_request + token = request.headers["Authorization"] + token = token.split(" ").last if token + @identity = Services::AuthService.new.authenticate!(token) + end + + private + + def handle_standard_error(exception) + render json: { error: exception.message }, status: :internal_server_error + end + + def handle_auth_error(exception) + render json: { error: "Unauthorized" }, status: :unauthorized + end end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 7a18e8a..8ef54bd 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -1,4 +1,6 @@ class CertificatesController < ApplicationController + before_action :authenticate_request + def create name = params[:common_name] || "host.example.com" ttl = params[:ttl] || "24h" diff --git a/app/lib/auth_error.rb b/app/lib/auth_error.rb new file mode 100644 index 0000000..61cb7bb --- /dev/null +++ b/app/lib/auth_error.rb @@ -0,0 +1,3 @@ +# Error representing a failed authentication +class AuthError < StandardError +end diff --git a/lib/services/auth_service.rb b/app/lib/services/auth_service.rb similarity index 61% rename from lib/services/auth_service.rb rename to app/lib/services/auth_service.rb index d51bff8..00a0796 100644 --- a/lib/services/auth_service.rb +++ b/app/lib/services/auth_service.rb @@ -7,5 +7,12 @@ def decode(jwt_token) rescue nil end + + def authenticate!(token) + identity = decode(token) + # TODO verify identity with authority + raise AuthError unless identity + identity + end end end diff --git a/lib/services/certificate_service.rb b/app/lib/services/certificate_service.rb similarity index 100% rename from lib/services/certificate_service.rb rename to app/lib/services/certificate_service.rb diff --git a/lib/services/vault_service.rb b/app/lib/services/vault_service.rb similarity index 100% rename from lib/services/vault_service.rb rename to app/lib/services/vault_service.rb From 3d91f309c14b1a6f8230bcd703d5c1bf36f8012e Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 19 Aug 2024 13:10:05 -0400 Subject: [PATCH 13/33] Add JWT decoding for cert requests, and better config support --- .devcontainer/docker-compose.yml | 1 + app/controllers/application_controller.rb | 3 ++- app/controllers/certificates_controller.rb | 4 +--- app/lib/services/auth_service.rb | 6 +++--- app/lib/services/certificate_service.rb | 6 +++--- app/lib/services/vault_service.rb | 12 ++++++------ config/application.rb | 3 +++ config/astral.yml | 9 +++++++++ test/controllers/certificates_controller_test.rb | 10 ++++++++-- 9 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 config/astral.yml diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 3e27d32..50cd8ff 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -20,6 +20,7 @@ services: environment: VAULT_ADDR: http://10.1.10.100:8200 VAULT_TOKEN: root_token + JWT_SIGNING_KEY: jwt_secret vault: image: hashicorp/vault:latest diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 686971b..4221d38 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,7 +1,8 @@ class ApplicationController < ActionController::API rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error - + attr_reader :identity # decoded and verified JWT + def info render json: { app: "astral", diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 8ef54bd..4241e78 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -2,9 +2,7 @@ class CertificatesController < ApplicationController before_action :authenticate_request def create - name = params[:common_name] || "host.example.com" - ttl = params[:ttl] || "24h" - cert = Services::CertificateService.new.get_cert(name, ttl) + cert = Services::CertificateService.new.get_cert_for identity render json: cert end end diff --git a/app/lib/services/auth_service.rb b/app/lib/services/auth_service.rb index 00a0796..f4344e1 100644 --- a/app/lib/services/auth_service.rb +++ b/app/lib/services/auth_service.rb @@ -1,8 +1,8 @@ module Services class AuthService - def decode(jwt_token) + def decode(token) # Decode a JWT access token using the configured base. - body = JWT.decode(token, ENV["JWT_TOKEN_BASE"])[0] + body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0] HashWithIndifferentAccess.new body rescue nil @@ -10,8 +10,8 @@ def decode(jwt_token) def authenticate!(token) identity = decode(token) - # TODO verify identity with authority raise AuthError unless identity + # TODO verify identity with authority identity end end diff --git a/app/lib/services/certificate_service.rb b/app/lib/services/certificate_service.rb index a8636b5..28a0364 100644 --- a/app/lib/services/certificate_service.rb +++ b/app/lib/services/certificate_service.rb @@ -1,12 +1,12 @@ module Services class CertificateService def initialize - # this should select an implementation service based on config + # TODO this should select an implementation service based on config @impl = VaultService.new end - def get_cert(common_name, ttl) - @impl.get_cert(common_name, ttl) + def get_cert_for(identity) + @impl.get_cert_for(identity) end end end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index 5cf2881..d801899 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -2,17 +2,17 @@ module Services class VaultService def initialize @client = Vault::Client.new( - address: ENV["VAULT_ADDR"], - token: ENV["VAULT_TOKEN"] + address: Rails.application.config.astral[:vault_addr], + token: Rails.application.config.astral[:vault_token] ) end - def get_cert(common_name, ttl) + def get_cert_for(identity) # Generate the TLS certificate using the intermediate CA tls_cert = @client.logical.write("pki_int/issue/learn", - common_name: common_name, - ttl: ttl, - ip_sans: "192.168.1.1", + common_name: identity[:common_name], + ttl: Rails.application.config.astral[:cert_ttl], + ip_sans: identity[:ip_sans], format: "pem") tls_cert.data end diff --git a/config/application.rb b/config/application.rb index a796394..b4b90a2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,5 +28,8 @@ class Application < Rails::Application # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true + + # Application configs from config/astral.yml + config.astral = config_for :astral end end diff --git a/config/astral.yml b/config/astral.yml new file mode 100644 index 0000000..390c4fd --- /dev/null +++ b/config/astral.yml @@ -0,0 +1,9 @@ +shared: + vault_addr: <%= ENV["VAULT_ADDR"] %> + vault_token: <%= ENV["VAULT_TOKEN"] %> + jwt_signing_key: <%= ENV["JWT_SIGNING_KEY"] %> + cert_ttl: 24h + +development: + +production: diff --git a/test/controllers/certificates_controller_test.rb b/test/controllers/certificates_controller_test.rb index 9811a14..24b01db 100644 --- a/test/controllers/certificates_controller_test.rb +++ b/test/controllers/certificates_controller_test.rb @@ -1,8 +1,14 @@ require "test_helper" class CertificatesControllerTest < ActionDispatch::IntegrationTest - test "create" do - post "/certificates" + test "create unauthorized" do + post certificates_path + assert_response :unauthorized + end + + test "create authorized" do + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" + post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" } assert_response :success %w[ ca_chain certificate From c40589c3ff9b886f4f9676e93b43feea76a7e8ae Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 19 Aug 2024 13:21:08 -0400 Subject: [PATCH 14/33] fix lint and ci unneeded --- .github/workflows/ci.yml | 18 +----------------- app/controllers/application_controller.rb | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66414b7..2654e8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,22 +22,6 @@ jobs: - name: Scan for common Rails security vulnerabilities using static analysis run: bin/brakeman --no-pager - scan_js: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - - name: Scan for security vulnerabilities in JavaScript dependencies - run: bin/importmap audit - lint: runs-on: ubuntu-latest steps: @@ -79,7 +63,7 @@ jobs: env: RAILS_ENV: test # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test test:system + run: bin/rails test - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4221d38..9e0c1a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ class ApplicationController < ActionController::API rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error attr_reader :identity # decoded and verified JWT - + def info render json: { app: "astral", From dddae9908a468dc452e07ea004f707cdb21a9fd5 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 19 Aug 2024 13:33:18 -0400 Subject: [PATCH 15/33] move integration test --- test/{controllers => integration}/certificates_controller_test.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{controllers => integration}/certificates_controller_test.rb (100%) diff --git a/test/controllers/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb similarity index 100% rename from test/controllers/certificates_controller_test.rb rename to test/integration/certificates_controller_test.rb From 9e01cf526a8003855166834ea87f054ccce2228d Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Mon, 19 Aug 2024 17:27:08 -0400 Subject: [PATCH 16/33] Update readme to show usage with JWT --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a28245..a28d48c 100644 --- a/README.md +++ b/README.md @@ -13,5 +13,13 @@ it will: This app is most easily run and developed in its devcontainer. 1) Open in devcontainer -2) Launch server -3) POST /certificates to acquire cert +2) Launch server using vscode launch config, or in terminal run: +``` +rails s +``` +3) POST /certificates to acquire cert in terminal: +``` +curl -X POST http://localhost:3000/certificates \ +-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" +``` + From a7597108644708bd8196461cbca2927ce1418faf Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 20 Aug 2024 10:42:43 -0400 Subject: [PATCH 17/33] one more jwt test to very signing --- app/lib/services/auth_service.rb | 3 ++- test/integration/certificates_controller_test.rb | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/lib/services/auth_service.rb b/app/lib/services/auth_service.rb index f4344e1..960f26d 100644 --- a/app/lib/services/auth_service.rb +++ b/app/lib/services/auth_service.rb @@ -4,7 +4,8 @@ def decode(token) # Decode a JWT access token using the configured base. body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0] HashWithIndifferentAccess.new body - rescue + rescue => e + Rails.logger.warn "Unable to decode token: #{e}" nil end diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index 24b01db..6ce5599 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -6,6 +6,12 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest assert_response :unauthorized end + test "create with faulty token" do + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn1.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" + post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" } + assert_response :unauthorized + end + test "create authorized" do jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" } From 82b46ce212675461fdf8e25a704c88c3367b597c Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 20 Aug 2024 10:45:04 -0400 Subject: [PATCH 18/33] clarify test --- test/integration/certificates_controller_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index 6ce5599..b025913 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -6,8 +6,8 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest assert_response :unauthorized end - test "create with faulty token" do - jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn1.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" + test "create with faulty token (encoded with different signing key)" do + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.gEUyaZcARiBQNq2RUwZU0MdFXqthyo_oSQ8DAgKvxCs" post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" } assert_response :unauthorized end From 1506027e1f5a9d021bcda4ff31aad9b381a9f4d8 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 21 Aug 2024 13:28:22 -0400 Subject: [PATCH 19/33] Added validation, full request modeling --- app/controllers/application_controller.rb | 6 ++ app/controllers/certificates_controller.rb | 33 +++++++- app/lib/services/certificate_service.rb | 4 +- app/lib/services/vault_service.rb | 10 +-- app/models/cert_isssue_request_test.rb | 77 +++++++++++++++++++ app/models/cert_issue_request.rb | 30 ++++++++ config/astral.yml | 2 +- .../certificates_controller_test.rb | 3 +- 8 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 app/models/cert_isssue_request_test.rb create mode 100644 app/models/cert_issue_request.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9e0c1a8..3eeffc5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,8 @@ class ApplicationController < ActionController::API rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error + rescue_from ActionController::ParameterMissing, with: :handle_bad_request + attr_reader :identity # decoded and verified JWT def info @@ -26,4 +28,8 @@ def handle_standard_error(exception) def handle_auth_error(exception) render json: { error: "Unauthorized" }, status: :unauthorized end + + def handle_bad_request(exception) + render json: { error: exception }, status: :bad_request + end end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 4241e78..51dd98b 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -2,7 +2,36 @@ class CertificatesController < ApplicationController before_action :authenticate_request def create - cert = Services::CertificateService.new.get_cert_for identity - render json: cert + req = CertIssueRequest.new(params_permitted) + if !req.valid? + render json: { error: req.errors}, status: :bad_request + else + cert = Services::CertificateService.new.get_cert_for identity, req + render json: cert + end + end + + private + + def params_permitted + attrs = %i[ + common_name + alt_names + exclude_cn_from_sans + format + not_after + other_sans + private_key_format + remove_roots_from_chain + ttl + uri_sans + ip_sans + serial_number + client_flag + code_signing_flag + email_protection_flag + server_flag + ] + params.permit(attrs) end end diff --git a/app/lib/services/certificate_service.rb b/app/lib/services/certificate_service.rb index 28a0364..c30ea06 100644 --- a/app/lib/services/certificate_service.rb +++ b/app/lib/services/certificate_service.rb @@ -5,8 +5,8 @@ def initialize @impl = VaultService.new end - def get_cert_for(identity) - @impl.get_cert_for(identity) + def get_cert_for(identity, cert_issue_request) + @impl.get_cert_for(identity, cert_issue_request) end end end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index d801899..8396333 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -7,13 +7,13 @@ def initialize ) end - def get_cert_for(identity) + def get_cert_for(identity, cert_issue_request) # Generate the TLS certificate using the intermediate CA tls_cert = @client.logical.write("pki_int/issue/learn", - common_name: identity[:common_name], - ttl: Rails.application.config.astral[:cert_ttl], - ip_sans: identity[:ip_sans], - format: "pem") + common_name: cert_issue_request.common_name, + ttl: cert_issue_request.ttl, + ip_sans: cert_issue_request.ip_sans, + format: cert_issue_request.format) tls_cert.data end end diff --git a/app/models/cert_isssue_request_test.rb b/app/models/cert_isssue_request_test.rb new file mode 100644 index 0000000..8738915 --- /dev/null +++ b/app/models/cert_isssue_request_test.rb @@ -0,0 +1,77 @@ +# test/models/cert_issue_request_test.rb +require "test_helper" + +class CertIssueRequestTest < ActiveSupport::TestCase + def setup + @attributes = { + common_name: "example.com", + alt_names: [ "alt1.example.com", "alt2.example.com" ], + exclude_cn_from_sans: true, + format: "der", + not_after: DateTime.now + 1.year, + other_sans: [ "other1", "other2" ], + private_key_format: "pkcs8", + remove_roots_from_chain: true, + ttl: 365, + uri_sans: [ "http://example.com" ], + ip_sans: [ "192.168.1.1" ], + serial_number: 123456, + client_flag: false, + code_signing_flag: true, + email_protection_flag: true, + server_flag: false + } + @cert_issue_request = CertIssueRequest.new(@attributes) + end + + test "should set attributes correctly" do + @attributes.each do |key, value| + assert_equal value, @cert_issue_request.send(key), "Attribute #{key} was not set correctly" + end + end + + test "should be valid with valid attributes" do + assert @cert_issue_request.valid? + end + + test "should require a common_name" do + @cert_issue_request.common_name = nil + assert_not @cert_issue_request.valid? + assert_includes @cert_issue_request.errors[:common_name], "can't be blank" + end + + test "should require a valid format" do + @cert_issue_request.format = "invalid_format" + assert_not @cert_issue_request.valid? + assert_includes @cert_issue_request.errors[:format], "is not included in the list" + end + + test "should require a valid private_key_format" do + @cert_issue_request.private_key_format = "invalid_format" + assert_not @cert_issue_request.valid? + assert_includes @cert_issue_request.errors[:private_key_format], "is not included in the list" + end + + test "should have default values" do + @cert_issue_request = CertIssueRequest.new + assert_equal false, @cert_issue_request.exclude_cn_from_sans + assert_equal "pem", @cert_issue_request.format + assert_equal "pem", @cert_issue_request.private_key_format + assert_equal false, @cert_issue_request.remove_roots_from_chain + assert_equal Rails.configuration.astral[:cert_ttl], @cert_issue_request.ttl + assert_equal true, @cert_issue_request.client_flag + assert_equal false, @cert_issue_request.code_signing_flag + assert_equal false, @cert_issue_request.email_protection_flag + assert_equal true, @cert_issue_request.server_flag + end + + test "should be invalid with default values" do + @cert_issue_request = CertIssueRequest.new + assert_not @cert_issue_request.valid? + end + + + test "fqdns should return alt_names plus common_name" do + assert_equal [ "alt1.example.com", "alt2.example.com", "example.com" ], @cert_issue_request.fqdns + end +end diff --git a/app/models/cert_issue_request.rb b/app/models/cert_issue_request.rb new file mode 100644 index 0000000..503845e --- /dev/null +++ b/app/models/cert_issue_request.rb @@ -0,0 +1,30 @@ +class CertIssueRequest + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :common_name, :string + attribute :alt_names, array: :string, default: [] + attribute :exclude_cn_from_sans, :boolean, default: false + attribute :format, :string, default: "pem" + attribute :not_after, :datetime + attribute :other_sans, array: :string, default: [] + attribute :private_key_format, :string, default: "pem" + attribute :remove_roots_from_chain, :boolean, default: false + attribute :ttl, :integer, default: Rails.configuration.astral[:cert_ttl] + attribute :uri_sans, array: :string, default: [] + attribute :ip_sans, array: :string, default: [] + attribute :serial_number, :integer + attribute :client_flag, :boolean, default: true + attribute :code_signing_flag, :boolean, default: false + attribute :email_protection_flag, :boolean, default: false + attribute :server_flag, :boolean, default: true + + validates :common_name, presence: true + validates :format, presence: true, inclusion: { in: %w[pem der pem_bundle] } + validates :private_key_format, presence: true, inclusion: { in: %w[pem der pkcs8] } + + + def fqdns + alt_names + [ common_name ] + end +end diff --git a/config/astral.yml b/config/astral.yml index 390c4fd..bee2f97 100644 --- a/config/astral.yml +++ b/config/astral.yml @@ -2,7 +2,7 @@ shared: vault_addr: <%= ENV["VAULT_ADDR"] %> vault_token: <%= ENV["VAULT_TOKEN"] %> jwt_signing_key: <%= ENV["JWT_SIGNING_KEY"] %> - cert_ttl: 24h + cert_ttl: <%= 24.hours.in_seconds %> development: diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index b025913..3df4e5e 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -14,7 +14,8 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest test "create authorized" do jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y" - post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" } + post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" }, + params: { common_name: "example.com" } assert_response :success %w[ ca_chain certificate From 9792fe6dd260a0a40d55a5e4a71108b745ac6d8a Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 21 Aug 2024 16:07:06 -0400 Subject: [PATCH 20/33] put test where it will run --- {app => test}/models/cert_isssue_request_test.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {app => test}/models/cert_isssue_request_test.rb (100%) diff --git a/app/models/cert_isssue_request_test.rb b/test/models/cert_isssue_request_test.rb similarity index 100% rename from app/models/cert_isssue_request_test.rb rename to test/models/cert_isssue_request_test.rb From 22e886bd8977f13ec28f6ca5f42e3bb752393343 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 21 Aug 2024 21:22:39 -0400 Subject: [PATCH 21/33] formatting --- app/controllers/certificates_controller.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 51dd98b..a6feed2 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -4,7 +4,7 @@ class CertificatesController < ApplicationController def create req = CertIssueRequest.new(params_permitted) if !req.valid? - render json: { error: req.errors}, status: :bad_request + render json: { error: req.errors }, status: :bad_request else cert = Services::CertificateService.new.get_cert_for identity, req render json: cert @@ -14,8 +14,7 @@ def create private def params_permitted - attrs = %i[ - common_name + attrs = %i[ common_name alt_names exclude_cn_from_sans format From 74687b477afa6e777772d1c5c90abe17931d21e7 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 21 Aug 2024 21:22:49 -0400 Subject: [PATCH 22/33] Add service method to check fqdn/auto-approved-groups with user groups claim --- app/lib/services/auth_service.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/lib/services/auth_service.rb b/app/lib/services/auth_service.rb index 960f26d..2873285 100644 --- a/app/lib/services/auth_service.rb +++ b/app/lib/services/auth_service.rb @@ -12,8 +12,15 @@ def decode(token) def authenticate!(token) identity = decode(token) raise AuthError unless identity - # TODO verify identity with authority + # TODO verify identity with authority? identity end + + def authorize!(identity, cert_req) + cert_req.fqdns.each do |fqdn| + domain = AppRegistryService.get_domain_name(fqdn) + raise AuthError unless (domain[:auto_approved_groups] & identity[:groups]).any? + end + end end end From 45b3195cb51d492a397a65b9c52ff2c7df19d374 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 11:54:13 -0400 Subject: [PATCH 23/33] Little refactor to separate app_registry from auth --- app/lib/services/app_registry_service.rb | 32 ++++++++++++++++++++++++ app/lib/services/auth_service.rb | 30 ++++++++-------------- app/lib/services/vault_service.rb | 2 +- config/astral.yml | 1 + 4 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 app/lib/services/app_registry_service.rb diff --git a/app/lib/services/app_registry_service.rb b/app/lib/services/app_registry_service.rb new file mode 100644 index 0000000..557523c --- /dev/null +++ b/app/lib/services/app_registry_service.rb @@ -0,0 +1,32 @@ +module Services + class AppRegistryService + def authenticate!(token) + identity = decode(token) + raise AuthError unless identity + # TODO verify identity with authority? + identity + end + + def authorize!(identity, cert_req) + cert_req.fqdns.each do |fqdn| + domain = get_domain_name(fqdn) + raise AuthError unless (domain[:auto_approved_groups] & identity[:groups]).any? + end + end + + private + + def decode(token) + # Decode a JWT access token using the configured base. + body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0] + HashWithIndifferentAccess.new body + rescue => e + Rails.logger.warn "Unable to decode token: #{e}" + nil + end + + def get_domain_name(fqdn) + # TODO implement + end + end +end diff --git a/app/lib/services/auth_service.rb b/app/lib/services/auth_service.rb index 2873285..a1e1764 100644 --- a/app/lib/services/auth_service.rb +++ b/app/lib/services/auth_service.rb @@ -1,26 +1,16 @@ module Services class AuthService - def decode(token) - # Decode a JWT access token using the configured base. - body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0] - HashWithIndifferentAccess.new body - rescue => e - Rails.logger.warn "Unable to decode token: #{e}" - nil - end + def initialize + # TODO make this selectable + @impl = AppRegistryService.new + end - def authenticate!(token) - identity = decode(token) - raise AuthError unless identity - # TODO verify identity with authority? - identity - end + def authenticate!(token) + @impl.authenticate!(token) + end - def authorize!(identity, cert_req) - cert_req.fqdns.each do |fqdn| - domain = AppRegistryService.get_domain_name(fqdn) - raise AuthError unless (domain[:auto_approved_groups] & identity[:groups]).any? - end - end + def authorize!(token, cert_issue_req) + @impl.authorize!(token, cert_issue_req) + end end end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index 8396333..5b032d3 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -9,7 +9,7 @@ def initialize def get_cert_for(identity, cert_issue_request) # Generate the TLS certificate using the intermediate CA - tls_cert = @client.logical.write("pki_int/issue/learn", + tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], common_name: cert_issue_request.common_name, ttl: cert_issue_request.ttl, ip_sans: cert_issue_request.ip_sans, diff --git a/config/astral.yml b/config/astral.yml index bee2f97..2872e54 100644 --- a/config/astral.yml +++ b/config/astral.yml @@ -1,6 +1,7 @@ shared: vault_addr: <%= ENV["VAULT_ADDR"] %> vault_token: <%= ENV["VAULT_TOKEN"] %> + vault_cert_path: "pki_int/issue/learn" jwt_signing_key: <%= ENV["JWT_SIGNING_KEY"] %> cert_ttl: <%= 24.hours.in_seconds %> From ecbd4ebb63ef6a560a34c0b94994994d4affd7b0 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 15:15:30 -0400 Subject: [PATCH 24/33] cert service doesn't need identity info --- app/lib/services/certificate_service.rb | 4 ++-- app/lib/services/vault_service.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/lib/services/certificate_service.rb b/app/lib/services/certificate_service.rb index c30ea06..3b63dfa 100644 --- a/app/lib/services/certificate_service.rb +++ b/app/lib/services/certificate_service.rb @@ -5,8 +5,8 @@ def initialize @impl = VaultService.new end - def get_cert_for(identity, cert_issue_request) - @impl.get_cert_for(identity, cert_issue_request) + def issue_cert(cert_issue_request) + @impl.issue_cert(cert_issue_request) end end end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index 5b032d3..efb2381 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -1,13 +1,14 @@ module Services class VaultService def initialize + # TODO create a new token for use in the session @client = Vault::Client.new( address: Rails.application.config.astral[:vault_addr], token: Rails.application.config.astral[:vault_token] ) end - def get_cert_for(identity, cert_issue_request) + def issue_cert(cert_issue_request) # Generate the TLS certificate using the intermediate CA tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], common_name: cert_issue_request.common_name, From 24abff0b429398d7dba4724c794671c15002aef5 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 15:40:46 -0400 Subject: [PATCH 25/33] Fix method reference --- app/controllers/certificates_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index a6feed2..4528663 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -6,7 +6,7 @@ def create if !req.valid? render json: { error: req.errors }, status: :bad_request else - cert = Services::CertificateService.new.get_cert_for identity, req + cert = Services::CertificateService.new.issue_cert(req) render json: cert end end From b6b5d754ab53aa9edb0d415aed8888864cf8e5d8 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:11:24 -0400 Subject: [PATCH 26/33] Fix lint --- app/lib/services/app_registry_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/services/app_registry_service.rb b/app/lib/services/app_registry_service.rb index 557523c..6b92cc2 100644 --- a/app/lib/services/app_registry_service.rb +++ b/app/lib/services/app_registry_service.rb @@ -15,7 +15,7 @@ def authorize!(identity, cert_req) end private - + def decode(token) # Decode a JWT access token using the configured base. body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0] @@ -24,7 +24,7 @@ def decode(token) Rails.logger.warn "Unable to decode token: #{e}" nil end - + def get_domain_name(fqdn) # TODO implement end From 844ca71283462d6222e4dcf1db26bd449c749631 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:15:22 -0400 Subject: [PATCH 27/33] Fix dependency scan --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fef8de0..9805647 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,7 +79,7 @@ GEM bigdecimal (3.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.1.2) + brakeman (6.2.0) racc builder (3.3.0) concurrent-ruby (1.3.4) From 2cb017e534baa887d8ca44da658d91aa4726a6ad Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:26:40 -0400 Subject: [PATCH 28/33] Try the devcontainer gh action --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2654e8d..00dc2b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true + - name: Set up DevContainer + uses: devcontainers/ci@v0.3 - name: Scan for common Rails security vulnerabilities using static analysis run: bin/brakeman --no-pager From 8f406a46522e5ec5a06aed4dc5fe302975a20cb9 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:31:03 -0400 Subject: [PATCH 29/33] try bundler --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00dc2b5..6041e41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: uses: devcontainers/ci@v0.3 - name: Scan for common Rails security vulnerabilities using static analysis - run: bin/brakeman --no-pager + run: bundle install && bin/brakeman --no-pager lint: runs-on: ubuntu-latest From 77c5fdb78071875ff326e82859a06ed5b2c972cd Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:32:52 -0400 Subject: [PATCH 30/33] try runCmd in gh action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6041e41..f1ede9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: uses: devcontainers/ci@v0.3 - name: Scan for common Rails security vulnerabilities using static analysis - run: bundle install && bin/brakeman --no-pager + runCmd: bin/brakeman --no-pager lint: runs-on: ubuntu-latest From 19814186c77705cc4823782fc606edcde53c10ca Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:35:03 -0400 Subject: [PATCH 31/33] tweak the yaml --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1ede9f..e4480c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,8 @@ jobs: - name: Set up DevContainer uses: devcontainers/ci@v0.3 - - - name: Scan for common Rails security vulnerabilities using static analysis - runCmd: bin/brakeman --no-pager + with: + runCmd: bin/brakeman --no-pager lint: runs-on: ubuntu-latest From 57bc1c8db1d501a612f30a010590c07dc5b41a1d Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:38:01 -0400 Subject: [PATCH 32/33] okay yet another dep up --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9805647..aefbe81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,7 +79,7 @@ GEM bigdecimal (3.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.2.0) + brakeman (6.2.1) racc builder (3.3.0) concurrent-ruby (1.3.4) From 2db0de81a12e172d05b29becba692a389ca224ef Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Thu, 22 Aug 2024 16:43:09 -0400 Subject: [PATCH 33/33] move test to devcontainer --- .github/workflows/ci.yml | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4480c7..fcf3b18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up DevContainer + - name: Run brakeman uses: devcontainers/ci@v0.3 with: runCmd: bin/brakeman --no-pager @@ -43,23 +43,13 @@ jobs: # - 6379:6379 # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - name: Install packages - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libsqlite3-0 libvips - - name: Checkout code uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - name: Run tests - env: - RAILS_ENV: test - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails test + uses: devcontainers/ci@v0.3 + with: + runCmd: bin/rails test - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4