Skip to content

Commit

Permalink
Store certificates in database and add custom provider
Browse files Browse the repository at this point in the history
* Read certbot generated files and store certificates in database.

* Allow creating custom certificates with manually entered certificate
  information.

* Add Rake task to reinstall all certificates from the database-

* Add Rake task to read installed certificates from the certbot output
  directory to upgrade a previous `cert_watch` install.
  • Loading branch information
tf committed Nov 6, 2017
1 parent b6585eb commit 1ac3479
Show file tree
Hide file tree
Showing 34 changed files with 540 additions and 78 deletions.
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
13 changes: 11 additions & 2 deletions admin/cert_watch/certificates.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module CertWatch
ActiveAdmin.register Certificate do
menu priority: 100

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

config.batch_actions = false

Expand All @@ -29,10 +29,14 @@ 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
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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
4 changes: 4 additions & 0 deletions lib/cert_watch/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Configuration
# Directory the web server reads pem files from
attr_accessor :pem_directory

# Directory the web server reads pem files from
attr_accessor :provider_install_directory_mapping

# Command to make server reload pem files
attr_accessor :server_reload_command

Expand All @@ -31,6 +34,7 @@ def initialize
@certbot_output_directory = '/etc/letsencrypt/live'

@pem_directory = '/etc/haproxy/ssl/'
@provider_install_directory_mapping = {}
@server_reload_command = '/etc/init.d/haproxy reload'
end
end
Expand Down
6 changes: 5 additions & 1 deletion lib/cert_watch/domain_owner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ def self.define(options)
new_value = self[attribute]

Certificate.find_by(domain: previous_value).try(:abandon)
Certificate.find_or_create_by(domain: new_value).renew if new_value.present?

if new_value.present?
certificate = Certificate.find_or_create_by(domain: new_value)
certificate.renew! if certificate.auto_renewable?
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/cert_watch/installer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module CertWatch
class PemDirectoryInstaller < Installer
class Installer
def install(_domain)
fail(NotImplementedError)
end
Expand Down
Loading

0 comments on commit 1ac3479

Please sign in to comment.