From 0811e217ed259068a9a41d6829316a288186f3d9 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Fri, 30 Aug 2024 20:23:12 -0400 Subject: [PATCH 1/6] Add jbuilder and moved the "info" view --- Gemfile | 3 +++ Gemfile.lock | 4 ++++ app/controllers/application_controller.rb | 13 +++++-------- app/controllers/info_controller.rb | 4 ++++ app/views/info/index.json.jbuilder | 3 +++ config/routes.rb | 2 +- 6 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 app/controllers/info_controller.rb create mode 100644 app/views/info/index.json.jbuilder diff --git a/Gemfile b/Gemfile index fb0fa71..c062096 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,9 @@ gem "vault" # Use the jwt gem to decode access tokens gem "jwt" +# Use the jbuilder gem +gem "jbuilder" + 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 217c45e..e3d37ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,9 @@ GEM irb (1.14.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + jbuilder (2.12.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) json (2.7.2) jwt (2.8.2) base64 @@ -272,6 +275,7 @@ DEPENDENCIES brakeman debug interactor (~> 3.0) + jbuilder jwt puma (>= 5.0) rails (~> 7.2.1) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2a48601..9c01126 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,18 +1,11 @@ class ApplicationController < ActionController::API + before_action :set_default_format 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 - render json: { - app: "astral", - description: "Astral provides a simplified API for PKI.", - version: "0.0.1" - } - end - def authenticate_request result = AuthenticateIdentity.call(request: request) if result.success? @@ -24,6 +17,10 @@ def authenticate_request private + def set_default_format + request.format = :json + end + def handle_standard_error(exception) render json: { error: exception.message }, status: :internal_server_error end diff --git a/app/controllers/info_controller.rb b/app/controllers/info_controller.rb new file mode 100644 index 0000000..1759a8c --- /dev/null +++ b/app/controllers/info_controller.rb @@ -0,0 +1,4 @@ +class InfoController < ApplicationController + def index + end +end diff --git a/app/views/info/index.json.jbuilder b/app/views/info/index.json.jbuilder new file mode 100644 index 0000000..6d812c9 --- /dev/null +++ b/app/views/info/index.json.jbuilder @@ -0,0 +1,3 @@ +json.app "astral" +json.description "Astral provides a simplified API for PKI." +json.version "0.0.1" diff --git a/config/routes.rb b/config/routes.rb index 0de2bf3..726a700 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,7 +10,7 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # Defines the root path route ("/") - root "application#info" + root "info#index" resources :certificates, only: %i[create] end From 1982e4b6528aa733970979cb0df299135ce44c4e Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 3 Sep 2024 15:42:39 -0400 Subject: [PATCH 2/6] Add cert create jbuilder view --- app/controllers/certificates_controller.rb | 6 ++---- app/lib/services/vault_service.rb | 7 ++----- app/views/certificates/create.json.jbuilder | 7 +++++++ 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 app/views/certificates/create.json.jbuilder diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 2deae83..e6ecae5 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -7,10 +7,8 @@ def create render json: { error: req.errors }, status: :bad_request end result = IssueCert.call(request: req) - if result.success? - # TODO use jbuilder to make the json - render json: result.cert - else + @cert = result.cert + if result.failure? raise StandardError.new result.message end end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index efb2381..da22157 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -9,12 +9,9 @@ def initialize end def issue_cert(cert_issue_request) + opts = cert_issue_request.to_hash # 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, - ttl: cert_issue_request.ttl, - ip_sans: cert_issue_request.ip_sans, - format: cert_issue_request.format) + tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], opts) tls_cert.data end end diff --git a/app/views/certificates/create.json.jbuilder b/app/views/certificates/create.json.jbuilder new file mode 100644 index 0000000..589f1fa --- /dev/null +++ b/app/views/certificates/create.json.jbuilder @@ -0,0 +1,7 @@ +json.ca_chain @cert.ca_chain +json.certificate @cert.certificate +json.expiration @cert.expiration +json.issuing_ca @cert.issuing_ca +json.private_key @cert.private_key +json.private_key_type @cert.private_key_format +json.serial_number @cert.serial_number From 565e5fb6c4895e58b2b7ea0cb264080327187554 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Tue, 3 Sep 2024 16:13:28 -0400 Subject: [PATCH 3/6] make sure all request attrs are passed down to vault api --- app/controllers/application_controller.rb | 5 ----- app/controllers/certificates_controller.rb | 4 +++- app/lib/services/vault_service.rb | 4 ++-- app/models/cert_issue_request.rb | 16 ++++++++++------ test/models/cert_isssue_request_test.rb | 10 +++++----- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9c01126..b07ccc1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,4 @@ class ApplicationController < ActionController::API - before_action :set_default_format rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error rescue_from ActionController::ParameterMissing, with: :handle_bad_request @@ -17,10 +16,6 @@ def authenticate_request private - def set_default_format - request.format = :json - end - def handle_standard_error(exception) render json: { error: exception.message }, status: :internal_server_error end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index e6ecae5..bef4b52 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -5,12 +5,14 @@ def create req = CertIssueRequest.new(params_permitted) if !req.valid? render json: { error: req.errors }, status: :bad_request + return end result = IssueCert.call(request: req) - @cert = result.cert if result.failure? raise StandardError.new result.message end + @cert = result.cert + render formats: :json end private diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index da22157..d6e77f8 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -9,10 +9,10 @@ def initialize end def issue_cert(cert_issue_request) - opts = cert_issue_request.to_hash + opts = cert_issue_request.attributes # Generate the TLS certificate using the intermediate CA tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], opts) - tls_cert.data + OpenStruct.new tls_cert.data end end end diff --git a/app/models/cert_issue_request.rb b/app/models/cert_issue_request.rb index 6496f93..6863b42 100644 --- a/app/models/cert_issue_request.rb +++ b/app/models/cert_issue_request.rb @@ -3,16 +3,16 @@ class CertIssueRequest include ActiveModel::Attributes attribute :common_name, :string - attribute :alt_names, array: :string, default: [] + attribute :alt_names, :string attribute :exclude_cn_from_sans, :boolean, default: false attribute :format, :string, default: "pem" attribute :not_after, :datetime - attribute :other_sans, array: :string, default: [] + attribute :other_sans, :string 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 :uri_sans, :string + attribute :ip_sans, :string attribute :serial_number, :integer attribute :client_flag, :boolean, default: true attribute :code_signing_flag, :boolean, default: false @@ -29,15 +29,19 @@ class CertIssueRequest validate :validate_no_wildcards def fqdns - alt_names + [ common_name ] + alt_names_array + [ common_name ] end def validate_no_wildcards if common_name.present? errors.add(:common_name, "cannot be a wildcard") if common_name.start_with? "*" end - alt_names.each do |fqdn| + alt_names_array.each do |fqdn| errors.add(:alt_names, "cannot include a wildcard") if fqdn.start_with? "*" end end + + def alt_names_array + (alt_names || "").split(",") + end end diff --git a/test/models/cert_isssue_request_test.rb b/test/models/cert_isssue_request_test.rb index 5ab8022..85db25b 100644 --- a/test/models/cert_isssue_request_test.rb +++ b/test/models/cert_isssue_request_test.rb @@ -5,16 +5,16 @@ class CertIssueRequestTest < ActiveSupport::TestCase def setup @attributes = { common_name: "example.com", - alt_names: [ "alt1.example.com", "alt2.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" ], + 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" ], + uri_sans: "http://example.com", + ip_sans: "192.168.1.1", serial_number: 123456, client_flag: false, code_signing_flag: true, @@ -71,7 +71,7 @@ def setup end test "#valid? should prevent wildcard alt_names" do - @cert_issue_request.alt_names = [ "www.example.com", "*.example.com" ] + @cert_issue_request.alt_names = "www.example.com,*.example.com" assert_not @cert_issue_request.valid? assert_includes @cert_issue_request.errors[:alt_names], "cannot include a wildcard" end From d1cf5c0cbbef480a98a026098c1d8a9bbefbb21f Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 4 Sep 2024 09:55:36 -0400 Subject: [PATCH 4/6] A few changes to params handling and README --- README.md | 20 ++++++++++++++----- app/controllers/application_controller.rb | 5 +++++ app/controllers/certificates_controller.rb | 20 ++----------------- .../certificates_controller_test.rb | 2 +- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 335d513..07c8890 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ Astral-rails is a proof-of-concept api application intended to simplify certificate acquisition for other applications/services. Broadly speaking, it will: -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). +1) Authenticate the request for cerficate using a third party trusted source (JWT, etc) +2) Authorize the request using a Domain Ownership Registry +3) If authorized, obtain a certificate from PKI CLM (such as Vault/OpenBao) +4) Log this transaction in audit infrastructure (ELK, etc). -# Running +# Running in development This app is most easily run and developed in its devcontainer. @@ -22,6 +23,15 @@ rails s curl -X POST http://localhost:3000/certificates \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI" \ -H "Content-type: application/json" \ --d "{ \"common_name\": \"example.com\" }" +-d "{ \"cert_issue_request\": { \"common_name\": \"example.com\" } }" ``` +# Running prod image +1) Build the prod image: +``` +docker build -t astral:latest . +``` +2) Run the prod image: +``` +docker run -e SECRET_KEY_BASE=mysecrit -p 3000:3000 astral:latest +``` diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b07ccc1..9c01126 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ class ApplicationController < ActionController::API + before_action :set_default_format rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error rescue_from ActionController::ParameterMissing, with: :handle_bad_request @@ -16,6 +17,10 @@ def authenticate_request private + def set_default_format + request.format = :json + end + def handle_standard_error(exception) render json: { error: exception.message }, status: :internal_server_error end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index bef4b52..744db81 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -18,23 +18,7 @@ def create 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) + attrs = CertIssueRequest.new.attributes.keys + params.require(:cert_issue_request).permit(attrs) end end diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index b21e156..7da04a3 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -15,7 +15,7 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest test "#create authorized" do jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI" post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" }, - params: { common_name: "example.com" } + params: { cert_issue_request: {common_name: "example.com" } } assert_response :success %w[ ca_chain certificate From 584dcb5f9ceee8062ef42a6f5759ddec70fe3983 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 4 Sep 2024 10:05:58 -0400 Subject: [PATCH 5/6] fix lint --- test/integration/certificates_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index 7da04a3..f952fa4 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -15,7 +15,7 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest test "#create authorized" do jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI" post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" }, - params: { cert_issue_request: {common_name: "example.com" } } + params: { cert_issue_request: { common_name: "example.com" } } assert_response :success %w[ ca_chain certificate From 43de77843f9c8e7a5667c43bd603c983e7800252 Mon Sep 17 00:00:00 2001 From: Geoff Wilson Date: Wed, 4 Sep 2024 10:10:24 -0400 Subject: [PATCH 6/6] Don't need this since request.format = "json" now --- app/controllers/certificates_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 744db81..3b3b3f6 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -12,7 +12,6 @@ def create raise StandardError.new result.message end @cert = result.cert - render formats: :json end private