Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store certificates in database and add custom provider #1

Merged
merged 5 commits into from
Jan 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# checks locally.
inherit_from: ./.rubocop_hound.yml

AllCops:
TargetRubyVersion: 2.1

# Use double quotes only for interpolation.
Style/StringLiterals:
EnforcedStyle: single_quotes
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ source 'https://rubygems.org'

gemspec

gem 'activeadmin'
gem 'state_machine', git: 'https://github.com/codevise/state_machine.git'

gem 'coveralls', require: false
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ CertWatch consists of the following components:
* Resque jobs to renew and install certificates.
* A mixin for models with a `cname` attribute to request certificats
on attribute change.
* Rake tasks to reinstall certificates on a fresh server

Optionally:

Expand Down Expand Up @@ -76,13 +77,27 @@ CertWatch.setup do |config|
# config.certbot_output_directory = '/etc/letsencrypt/live'

# Directory the web server reads pem files from
# config.pem_directory = '/etc/haproxy/ssl/
# config.pem_directory = '/etc/haproxy/ssl/'

# Place pem files in provider specific subdirectories of pem directory.
# By default, all pem files are placed in pem directory itself.
# config.provider_install_directory_mapping = {
# certbot: 'letsencrypt',
# custom: 'custom'
# }

# Command to make server reload pem files
# config.server_reload_command = '/etc/init.d/haproxy reload'
end
```

Ensure private keys do not show up in log files:

```ruby
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:private_key]
```

Include the `DomainOwner` mixin into a model with a domain
attribute. This makes CertWatch obtain or renew certificates whenever
the attribute changes. Validation has to be provided by the host
Expand All @@ -104,7 +119,7 @@ to the top of your Active Admin initializer:
ActiveAdmin.application.load_paths.unshift(CertWatch.active_admin_load_path)
```

If you use the CanCan authorization adapter, you also need add the
If you use the CanCan authorization adapter, you also need to add the
following rule for users that should be allowed to manage certificats:

```ruby
Expand Down Expand Up @@ -135,6 +150,21 @@ fetch_billed_traffic_usages:
Finally ensure Resque workers have been assigned to the `cert_watch`
queue.

## Rake Tasks

Add the following line to your application's `Rakefile`:

```ruby
# Rakefile
require 'cert_watch/tasks'
```

To reinstall all certificates (i.e. on a new server), run:

```
$ bin/rake cert_watch:reinstall:all
```

## Active Admin View Components

You can render a status tag displaying the current certificate state
Expand Down
29 changes: 19 additions & 10 deletions admin/cert_watch/certificates.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
require 'cert_watch/views/all'

module CertWatch
ActiveAdmin.register Certificate do
ActiveAdmin.register Certificate, as: 'Certificate' do
menu priority: 100

actions :index, :new, :create, :show
actions :index, :new, :create, :show, :edit, :update

config.batch_actions = false

index do
column :domain do |certificate|
link_to(certificate.domain, admin_cert_watch_certificate_path(certificate))
link_to(certificate.domain, admin_certificate_path(certificate))
end
column :state do |certificate|
cert_watch_certificate_state(certificate)
Expand All @@ -29,18 +29,22 @@ module CertWatch

filter :domain
filter :last_renewed_at
filter :provider, as: :select, collection: Certificate::PROVIDERS

form do |f|
f.inputs do
f.input :domain
f.input :public_key
f.input :private_key
f.input :chain
end
f.actions
end

action_item(only: :show) do
action_item(:renew, only: :show) do
if resource.can_renew?
button_to(I18n.t('cert_watch.admin.certificates.renew'),
renew_admin_cert_watch_certificate_path(resource),
renew_admin_certificate_path(resource),
method: :post,
data: {
rel: 'renew',
Expand All @@ -49,10 +53,10 @@ module CertWatch
end
end

action_item(only: :show) do
action_item(:install, only: :show) do
if resource.can_install?
button_to(I18n.t('cert_watch.admin.certificates.install'),
install_admin_cert_watch_certificate_path(resource),
install_admin_certificate_path(resource),
method: :post,
data: {
rel: 'install',
Expand All @@ -64,13 +68,13 @@ module CertWatch
member_action :renew, method: :post do
resource = Certificate.find(params[:id])
resource.renew
redirect_to(admin_cert_watch_certificate_path(resource))
redirect_to(admin_certificate_path(resource))
end

member_action :install, method: :post do
resource = Certificate.find(params[:id])
resource.install
redirect_to(admin_cert_watch_certificate_path(resource))
redirect_to(admin_certificate_path(resource))
end

show title: :domain do |certificate|
Expand All @@ -84,12 +88,17 @@ module CertWatch
row :last_renewal_failed_at
row :last_installed_at
row :last_install_failed_at
row :public_key
end
end

before_create do |certificate|
certificate.provider = 'custom'
end

controller do
def permitted_params
params.permit(cert_watch_certificate: [:domain])
params.permit(certificate: [:domain, :public_key, :private_key, :chain])
end
end
end
Expand Down
8 changes: 7 additions & 1 deletion app/jobs/cert_watch/install_certificate_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ class InstallCertificateJob
@queue = :cert_watch

def self.perform_with_result(certificate, _options = {})
CertWatch.installer.install(certificate.domain)
CertWatch.installer.install(domain: certificate.domain,
provider: certificate.provider,
public_key: certificate.public_key,
private_key: certificate.private_key,
chain: certificate.chain)

certificate.last_installed_at = Time.now

:ok
rescue InstallError
certificate.last_install_failed_at = Time.now
Expand Down
5 changes: 4 additions & 1 deletion app/jobs/cert_watch/renew_certificate_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ class RenewCertificateJob
@queue = :cert_watch

def self.perform_with_result(certificate, _options = {})
CertWatch.client.renew(certificate.domain)
result = CertWatch.client.renew(certificate.domain)

certificate.attributes = result.slice(:public_key, :private_key, :chain)
certificate.last_renewed_at = Time.now

:ok
rescue RenewError
certificate.last_renewal_failed_at = Time.now
Expand Down
1 change: 1 addition & 0 deletions app/jobs/cert_watch/renew_expiring_certificates_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class RenewExpiringCertificatesJob

def self.perform
Certificate
.auto_renewable
.installed
.expiring
.limit(CertWatch.config.renewal_batch_size)
Expand Down
30 changes: 30 additions & 0 deletions app/models/cert_watch/certificate.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module CertWatch
class Certificate < ActiveRecord::Base
PROVIDERS = %w(certbot custom).freeze

validates :provider, inclusion: PROVIDERS

state_machine initial: 'not_installed' do
extend StateMachineJob::Macro

Expand All @@ -12,6 +16,7 @@ class Certificate < ActiveRecord::Base
end

event :install do
transition 'not_installed' => 'installing'
transition 'installed' => 'installing'
transition 'installing_failed' => 'installing'
end
Expand All @@ -38,5 +43,30 @@ class Certificate < ActiveRecord::Base
scope(:failed, -> { where(state: %w(renewing_failed installing_failed)) })
scope(:expiring, -> { where('last_renewed_at < ?', CertWatch.config.renewal_interval.ago) })
scope(:abandoned, -> { where(state: 'abandoned') })

scope(:auto_renewable, -> { where.not(provider: 'custom') })
scope(:custom, -> { where(provider: 'custom') })

def can_renew?
auto_renewable? && super
end

def can_install?
complete? && super
end

def auto_renewable?
!custom?
end

def custom?
provider == 'custom'
end

def complete?
public_key.present? &&
private_key.present? &&
chain.present?
end
end
end
8 changes: 8 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ de:
confirm_install: "Soll das Zertifikat wirklich installiert werden?"
activerecord:
models:
# Required for ActiveAdmin navigation
certificate:
one: "SSL Zertifikat"
other: "SSL Zertifikate"
"cert_watch/certificate":
one: "SSL Zertifikat"
other: "SSL Zertifikate"
Expand All @@ -39,6 +43,10 @@ de:
last_renewal_failed_at: "Erneuerung fehlgeschlagen"
last_installed_at: "Zuletzt installiert"
last_install_failed_at: "Installation fehlgeschlagen"
provider: "Typ"
public_key: "Zertifikat"
private_key: "Privater Schlüssel"
chain: "Zertifikatskette"
active_admin:
scopes:
installed: "Installiert"
Expand Down
8 changes: 8 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ en:
confirm_install: "Are you sure you want to reinstall this certificate?"
activerecord:
models:
# Required for ActiveAdmin navigation
certificate:
one: "SSL Certificate"
other: "SSL Certificates"
"cert_watch/certificate":
one: "SSL Certificate"
other: "SSL Certificates"
Expand All @@ -39,6 +43,10 @@ en:
last_renewal_failed_at: "Last failed renewal"
last_installed_at: "Last installed at"
last_install_failed_at: "Last failed install"
provider: "Type"
public_key: "Certificate"
private_key: "Private Key"
chain: "Intermediate Certificates"
active_admin:
scopes:
installed: "Installed"
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20171023122819_add_key_fields_to_certificates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddKeyFieldsToCertificates < ActiveRecord::Migration
def change
add_column :cert_watch_certificates, :public_key, :text
add_column :cert_watch_certificates, :private_key, :text
add_column :cert_watch_certificates, :chain, :text
end
end
5 changes: 5 additions & 0 deletions db/migrate/20171024073152_add_provider_to_certificates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddProviderToCertificates < ActiveRecord::Migration
def change
add_column :cert_watch_certificates, :provider, :string, default: 'certbot', null: false
end
end
13 changes: 8 additions & 5 deletions lib/cert_watch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ def self.setup
yield @config if block_given?

self.client = CertbotClient.new(executable: config.certbot_executable,
port: config.certbot_port)

self.installer = PemDirectoryInstaller.new(pem_directory: config.pem_directory,
input_directory: config.certbot_output_directory,
reload_command: config.server_reload_command)
port: config.certbot_port,
output_directory: config.certbot_output_directory)

self.installer =
PemDirectoryInstaller
.new(pem_directory: config.pem_directory,
provider_directory_mapping: config.provider_install_directory_mapping,
reload_command: config.server_reload_command)
end

mattr_accessor :client
Expand Down
15 changes: 15 additions & 0 deletions lib/cert_watch/certbot_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class CertbotClient < Client
def initialize(options)
@executable = options.fetch(:executable)
@port = options.fetch(:port)
@output_directory = options.fetch(:output_directory)
@shell = options.fetch(:shell, Shell)
end

Expand All @@ -13,10 +14,20 @@ def renew(domain)
end

@shell.sudo(renew_command(domain))

read_outputs(domain)
rescue Shell::CommandFailed => e
fail(RenewError, e.message)
end

def read_outputs(domain)
{
public_key: read_output(domain, 'cert.pem'),
private_key: read_output(domain, 'privkey.pem'),
chain: read_output(domain, 'chain.pem')
}
end

private

def renew_command(domain)
Expand All @@ -28,5 +39,9 @@ def flags
'--agree-tos --renew-by-default ' \
"--standalone --standalone-supported-challenges http-01 --http-01-port #{@port}"
end

def read_output(domain, file_name)
@shell.sudo_read(File.join(@output_directory, domain, file_name))
end
end
end
4 changes: 4 additions & 0 deletions lib/cert_watch/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ class Client
def renew(_domain)
fail(NotImplementedError)
end

def read_outputs(_domain)
fail(NotImplementedError)
end
end
end
Loading