cert_watch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +151 -0
  4. data/Rakefile +16 -0
  5. data/app/assets/javascripts/cert_watch/application.js +13 -0
  6. data/app/assets/stylesheets/cert_watch/application.css +15 -0
  7. data/app/controllers/cert_watch/application_controller.rb +5 -0
  8. data/app/jobs/cert_watch/install_certificate_job.rb +16 -0
  9. data/app/jobs/cert_watch/renew_certificate_job.rb +16 -0
  10. data/app/jobs/cert_watch/renew_expiring_certificates_job.rb +13 -0
  11. data/app/models/cert_watch/certificate.rb +42 -0
  12. data/app/views/layouts/cert_watch/application.html.erb +14 -0
  13. data/config/locales/de.yml +47 -0
  14. data/config/locales/en.yml +47 -0
  15. data/config/routes.rb +2 -0
  16. data/db/migrate/20160711193700_create_certificates.rb +18 -0
  17. data/lib/cert_watch/CHANGELOG.md +5 -0
  18. data/lib/cert_watch/certbot_client.rb +32 -0
  19. data/lib/cert_watch/client.rb +7 -0
  20. data/lib/cert_watch/configuration.rb +37 -0
  21. data/lib/cert_watch/domain_owner.rb +23 -0
  22. data/lib/cert_watch/engine.rb +10 -0
  23. data/lib/cert_watch/error.rb +4 -0
  24. data/lib/cert_watch/install_error.rb +4 -0
  25. data/lib/cert_watch/installer.rb +7 -0
  26. data/lib/cert_watch/pem_directory_installer.rb +55 -0
  27. data/lib/cert_watch/renew_error.rb +4 -0
  28. data/lib/cert_watch/sanitize.rb +13 -0
  29. data/lib/cert_watch/shell.rb +20 -0
  30. data/lib/cert_watch/version.rb +3 -0
  31. data/lib/cert_watch/views/all.rb +3 -0
  32. data/lib/cert_watch/views/certificate_state.rb +42 -0
  33. data/lib/cert_watch.rb +32 -0
  34. data/spec/cert_watch/certbot_client_spec.rb +53 -0
  35. data/spec/cert_watch/domain_owner_spec.rb +62 -0
  36. data/spec/cert_watch/pem_directory_installer_spec.rb +75 -0
  37. data/spec/cert_watch/sanitize_spec.rb +19 -0
  38. data/spec/cert_watch/shell_spec.rb +19 -0
  39. data/spec/cert_watch/views/certificate_state_spec.rb +33 -0
  40. data/spec/examples.txt +40 -0
  41. data/spec/factories/certificates.rb +6 -0
  42. data/spec/internal/config/database.yml +3 -0
  43. data/spec/internal/config/routes.rb +3 -0
  44. data/spec/internal/db/combustion_test.sqlite +0 -0
  45. data/spec/internal/db/schema.rb +7 -0
  46. data/spec/internal/log/jobs/test/cert_watch.log +3793 -0
  47. data/spec/internal/log/test.log +24187 -0
  48. data/spec/internal/public/favicon.ico +0 -0
  49. data/spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb +43 -0
  50. data/spec/models/cert_watch/certificate_spec.rb +102 -0
  51. data/spec/rails_helper.rb +45 -0
  52. data/spec/spec_helper.rb +98 -0
  53. data/spec/support/config/cert_watch.rb +7 -0
  54. data/spec/support/config/factory_girl.rb +11 -0
  55. data/spec/support/config/resque_logger.rb +16 -0
  56. data/spec/support/config/timecop.rb +21 -0
  57. data/spec/support/helpers/doubles.rb +28 -0
  58. data/spec/support/helpers/fixtures.rb +25 -0
  59. data/spec/support/helpers/inline_resque.rb +9 -0
  60. data/spec/support/helpers/view_component_example_group.rb +31 -0
  61. metadata +298 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0235895da21094eabbdc37b3641a63a5fcfc2b3c
4
+ data.tar.gz: 192222872b10fc655735666b4d06e0c0e997ec2a
5
+ SHA512:
6
+ metadata.gz: f9f00d934bf03f0fbfb3b2c2b3918acb4c3e1a2e6c6d22ee91c6bbc33835945048a9eaddc2ff28945a64d9bc1f00f24dadfde3f6941239b85cd360842d4cae92
7
+ data.tar.gz: cbed0377537e874fe030f1c2aaca33b5d7aaf9a6756b6628c2fdd71c4dc64349a3f03cf70f940101775395df9a1ce8cae548348056be40b770ebc3844690652c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Tim Fischbach
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # CertWatch
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/cert_watch.svg)](http://badge.fury.io/rb/cert_watch)
4
+ [![Dependency Status](https://gemnasium.com/badges/github.com/codevise/cert_watch.svg)](https://gemnasium.com/github.com/codevise/cert_watch)
5
+ [![Build Status](https://travis-ci.org/codevise/cert_watch.svg?branch=master)](https://travis-ci.org/codevise/cert_watch)
6
+ [![Coverage Status](https://coveralls.io/repos/github/codevise/cert_watch/badge.svg?branch=master)](https://coveralls.io/github/codevise/cert_watch?branch=master)
7
+ [![Code Climate](https://codeclimate.com/github/codevise/cert_watch/badges/gpa.svg)](https://codeclimate.com/github/codevise/cert_watch)
8
+
9
+ A Rails engine to manage and automatically obtain, install and renew
10
+ SSL certificates.
11
+
12
+ ## Ingredients
13
+
14
+ CertWatch consists of the following components:
15
+
16
+ * Resque jobs to renew and install certificates.
17
+ * A mixin for models with a `cname` attribute to request certificats
18
+ on attribute change.
19
+
20
+ Optionally:
21
+
22
+ * An Active Admin resource to manage certificates.
23
+ * An Arbre view component to display certificate status for a given
24
+ domain.
25
+
26
+ ## Requirements
27
+
28
+ * Rails 4
29
+ * [Resque](https://github.com/resque/resque)
30
+ * [Resque Scheduler](https://github.com/resque/resque-scheduler)
31
+ * [Resque Logger](https://github.com/salizzar/resque-logger)
32
+ * [Certbot](https://certbot.eff.org/)
33
+
34
+ ## Limitations
35
+
36
+ * Requires `sudo` on the server. The `certbot` script used to obtain
37
+ certificates needs root priviledges. This could probably be avoided
38
+ by using the
39
+ [`acme-client` gem](https://github.com/unixcharles/acme-client)
40
+ instead.
41
+ * Works only with webservers that can read certificates from a
42
+ directory (Tested with HAProxy).
43
+
44
+ ## Installation
45
+
46
+ Add the following lines to your `Gemfile` and run `bundle install`:
47
+
48
+ ```ruby
49
+ gem 'cert_watch'
50
+
51
+ # Required since state_machine gem is unmaintained
52
+ gem 'state_machine', git: 'https://github.com/codevise/state_machine.git'
53
+ ```
54
+
55
+ Add an initializer:
56
+
57
+ ```ruby
58
+ # config/initializers/cert_watch.rb
59
+ CertWatch.setup do |config|
60
+ # Uncomment any of the below options to change the default
61
+
62
+ # Maximum age of certificates before renewal.
63
+ # config.renewal_interval = 1.month
64
+
65
+ # Number of expiring certificates to renew in one run of the
66
+ # `RenewExpiringCertificatesJob`.
67
+ # config.renewal_batch_size = 10
68
+
69
+ # File name of the certbot executable.
70
+ # config.certbot_executable = '/usr/local/share/letsencrypt/bin/certbot'
71
+
72
+ # Port for the standalone certbot HTTP server
73
+ # config.certbot_port = 9999
74
+
75
+ # Directory certbot outputs certificates to
76
+ # config.certbot_output_directory = '/etc/letsencrypt/live'
77
+
78
+ # Directory the web server reads pem files from
79
+ # config.pem_directory = '/etc/haproxy/ssl/
80
+
81
+ # Command to make server reload pem files
82
+ # config.server_reload_command = '/etc/init.d/haproxy reload'
83
+ end
84
+ ```
85
+
86
+ Include the `DomainOwner` mixin into a model with a domain
87
+ attribute. This makes CertWatch obtain or renew certificates whenever
88
+ the attribute changes. Validation has to be provided by the host
89
+ application.
90
+
91
+ ```ruby
92
+ # app/models/account.rb
93
+ # assuming Account has a cname attribute
94
+ class Account
95
+ include CertWatch.domain_owner(attribute: :cname)
96
+ end
97
+ ```
98
+
99
+ If you want to use the Active Admin resource, add the following line
100
+ to the top of your Active Admin initializer:
101
+
102
+ ```ruby
103
+ # config/initializers/active_admin.rb
104
+ ActiveAdmin.application.load_paths.unshift(CertWatch.active_admin_load_path)
105
+ ```
106
+
107
+ If you use the CanCan authorization adapter, you also need add the
108
+ following rule for users that should be allowed to manage certificats:
109
+
110
+ ```ruby
111
+ # app/models/ability.rb
112
+ can :manage, CertWatch::Certificate
113
+ ```
114
+
115
+ Now install migrations and migrate your database:
116
+
117
+ ```
118
+ $ bin/rake cert_watch:install:migrations
119
+ $ bin/rake db:migrate
120
+ ```
121
+
122
+ Setup your `resque_schedule.yml` to check for expiring certificates:
123
+
124
+ ```
125
+ # config/resque_schedule.yml
126
+ fetch_billed_traffic_usages:
127
+ every:
128
+ - "5h"
129
+ - :first_in: "1m"
130
+ class: "CertWatch::RenewExpiringCertificatesJob"
131
+ queue: cert_watch
132
+ description: "Check for expiring SSL certificates"
133
+ ```
134
+
135
+ Finally ensure Resque workers have been assigned to the `cert_watch`
136
+ queue.
137
+
138
+ ## Active Admin View Components
139
+
140
+ You can render a status tag displaying the current certificate state
141
+ for a given domain:
142
+
143
+ ```ruby
144
+ # app/admin/dashboard.rb
145
+ require 'cert_watch/views/certificate_state'
146
+
147
+ div(class: 'account_cname') do
148
+ text_node(account.cname)
149
+ cert_watch_certificate_state(account.cname)
150
+ end
151
+ ```
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ require 'bundler/gem_tasks'
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ require 'semmy'
14
+ Semmy::Tasks.install
15
+
16
+ task default: :spec
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ module CertWatch
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module CertWatch
2
+ class InstallCertificateJob
3
+ extend StateMachineJob
4
+
5
+ @queue = :cert_watch
6
+
7
+ def self.perform_with_result(certificate, _options = {})
8
+ CertWatch.installer.install(certificate.domain)
9
+ certificate.last_installed_at = Time.now
10
+ :ok
11
+ rescue InstallError
12
+ certificate.last_install_failed_at = Time.now
13
+ fail
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module CertWatch
2
+ class RenewCertificateJob
3
+ extend StateMachineJob
4
+
5
+ @queue = :cert_watch
6
+
7
+ def self.perform_with_result(certificate, _options = {})
8
+ CertWatch.client.renew(certificate.domain)
9
+ certificate.last_renewed_at = Time.now
10
+ :ok
11
+ rescue RenewError
12
+ certificate.last_renewal_failed_at = Time.now
13
+ fail
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module CertWatch
2
+ class RenewExpiringCertificatesJob
3
+ @queue = :cert_watch
4
+
5
+ def self.perform
6
+ Certificate
7
+ .installed
8
+ .expiring
9
+ .limit(CertWatch.config.renewal_batch_size)
10
+ .each(&:renew)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ module CertWatch
2
+ class Certificate < ActiveRecord::Base
3
+ state_machine initial: 'not_installed' do
4
+ extend StateMachineJob::Macro
5
+
6
+ event :renew do
7
+ transition 'not_installed' => 'renewing'
8
+ transition 'installed' => 'renewing'
9
+ transition 'renewing_failed' => 'renewing'
10
+ transition 'installing_failed' => 'renewing'
11
+ transition 'abandoned' => 'renewing'
12
+ end
13
+
14
+ event :install do
15
+ transition 'installed' => 'installing'
16
+ transition 'installing_failed' => 'installing'
17
+ end
18
+
19
+ job RenewCertificateJob do
20
+ on_enter 'renewing'
21
+ result ok: 'installing'
22
+ result error: 'renewing_failed'
23
+ end
24
+
25
+ job InstallCertificateJob do
26
+ on_enter 'installing'
27
+ result ok: 'installed'
28
+ result error: 'installing_failed'
29
+ end
30
+
31
+ event :abandon do
32
+ transition any => 'abandoned'
33
+ end
34
+ end
35
+
36
+ scope(:installed, -> { where(state: 'installed') })
37
+ scope(:processing, -> { where(state: %w(renewing installing)) })
38
+ scope(:failed, -> { where(state: %w(renewing_failed installing_failed)) })
39
+ scope(:expiring, -> { where('last_renewed_at < ?', CertWatch.config.renewal_interval.ago) })
40
+ scope(:abandoned, -> { where(state: 'abandoned') })
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>CertWatch</title>
5
+ <%= stylesheet_link_tag "cert_watch/application", media: "all" %>
6
+ <%= javascript_include_tag "cert_watch/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,47 @@
1
+ de:
2
+ cert_watch:
3
+ states:
4
+ short:
5
+ not_found: "Nicht installiert"
6
+ not_installed: "Nicht installiert"
7
+ renewing: "Wird erneuert..."
8
+ renewing_failed: "Erneuerung fehlgeschlagen"
9
+ installing: "Wird installiert..."
10
+ installing_failed: "Installation fehlgeschlagen"
11
+ installed: "Installiert"
12
+ abandoned: "Unbenutzt"
13
+ long:
14
+ not_found: "SSL nicht installiert"
15
+ not_installed: "SSL nicht installiert"
16
+ renewing: "SSL wird eingerichtet..."
17
+ renewing_failed: "SSL Erneuerung fehlgeschlagen"
18
+ installing: "SSL wird installiert..."
19
+ installing_failed: "SSL Installation fehlgeschlagen"
20
+ installed: "SSL installiert"
21
+ abandoned: "SSL Zertifikat unbenutzt"
22
+ admin:
23
+ certificates:
24
+ renew: "Erneuern"
25
+ confirm_renew: "Soll das Zertifikat wirklich erneuert werden?"
26
+ install: "Installieren"
27
+ confirm_install: "Soll das Zertifikat wirklich installiert werden?"
28
+ activerecord:
29
+ models:
30
+ "cert_watch/certificate":
31
+ one: "SSL Zertifikat"
32
+ other: "SSL Zertifikate"
33
+ attributes:
34
+ "cert_watch/certificate":
35
+ domain: "Domain"
36
+ state: "Status"
37
+ created_at: "Erstellt am"
38
+ last_renewed_at: "Zuletzt erneuert"
39
+ last_renewal_failed_at: "Erneuerung fehlgeschlagen"
40
+ last_installed_at: "Zuletzt installiert"
41
+ last_install_failed_at: "Installation fehlgeschlagen"
42
+ active_admin:
43
+ scopes:
44
+ installed: "Installiert"
45
+ processing: "In Verarbeitung"
46
+ failed: "Fehlgeschlagen"
47
+ abandoned: "Unbenutzt"
@@ -0,0 +1,47 @@
1
+ en:
2
+ cert_watch:
3
+ states:
4
+ short:
5
+ not_found: "Not installed"
6
+ not_installed: "Not installed"
7
+ renewing: "Renewing..."
8
+ renewing_failed: "Renewing failed"
9
+ installing: "Installing..."
10
+ installing_failed: "Installing failed"
11
+ installed: "Installed"
12
+ abandoned: "Unused"
13
+ long:
14
+ not_found: "SSL not installed"
15
+ not_installed: "SSL not installed"
16
+ renewing: "SSL is being renewed..."
17
+ renewing_failed: "SSL renewal failed"
18
+ installing: "SSL is being installed..."
19
+ installing_failed: "SSL install failed "
20
+ installed: "SSL installed"
21
+ abandoned: "SSL certificate unused"
22
+ admin:
23
+ certificates:
24
+ renew: "Renew"
25
+ confirm_renew: "Are you sure you want to renew the certificate?"
26
+ install: "Install"
27
+ confirm_install: "Are you sure you want to reinstall this certificate?"
28
+ activerecord:
29
+ models:
30
+ "cert_watch/certificate":
31
+ one: "SSL Certificate"
32
+ other: "SSL Certificates"
33
+ attributes:
34
+ "cert_watch/certificate":
35
+ domain: "Domain"
36
+ state: "State"
37
+ created_at: "Created at"
38
+ last_renewed_at: "Last renewed at"
39
+ last_renewal_failed_at: "Last failed renewal"
40
+ last_installed_at: "Last installed at"
41
+ last_install_failed_at: "Last failed install"
42
+ active_admin:
43
+ scopes:
44
+ installed: "Installed"
45
+ processing: "Processing"
46
+ failed: "Failed"
47
+ abandoned: "Unused"
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ CertWatch::Engine.routes.draw do
2
+ end
@@ -0,0 +1,18 @@
1
+ class CreateCertificates < ActiveRecord::Migration
2
+ def change
3
+ create_table :cert_watch_certificates do |t|
4
+ t.string :state, default: 'not_installed', null: false
5
+ t.string :domain
6
+
7
+ t.datetime :last_renewed_at
8
+ t.datetime :last_renewal_failed_at
9
+
10
+ t.datetime :last_installed_at
11
+ t.datetime :last_install_failed_at
12
+
13
+ t.timestamps null: false
14
+ end
15
+
16
+ add_index 'cert_watch_certificates', ['domain'], name: 'index_cert_watch_certificates_on_domain', using: :btree
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ### Version 1.0.0
4
+
5
+ Initial release.
@@ -0,0 +1,32 @@
1
+ module CertWatch
2
+ class CertbotClient < Client
3
+ def initialize(options)
4
+ @executable = options.fetch(:executable)
5
+ @port = options.fetch(:port)
6
+ @shell = options.fetch(:shell, Shell)
7
+ end
8
+
9
+ def renew(domain)
10
+ if Rails.env.development?
11
+ Rails.logger.info("[CertWatch] Skipping certificate renewal for #{domain} in development.")
12
+ return
13
+ end
14
+
15
+ @shell.sudo(renew_command(domain))
16
+ rescue Shell::CommandFailed => e
17
+ fail(RenewError, e.message)
18
+ end
19
+
20
+ private
21
+
22
+ def renew_command(domain)
23
+ Sanitize.check_domain!(domain)
24
+ "#{@executable} certonly #{flags} -d #{domain}"
25
+ end
26
+
27
+ def flags
28
+ '--agree-tos --renew-by-default ' \
29
+ "--standalone --standalone-supported-challenges http-01 --http-01-port #{@port}"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module CertWatch
2
+ class Client
3
+ def renew(_domain)
4
+ fail(NotImplementedError)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module CertWatch
2
+ class Configuration
3
+ # Maximum age of certificates before renewal.
4
+ attr_accessor :renewal_interval
5
+
6
+ # Number of expiring certificates to renew in one run of the
7
+ # `RenewExpiringCertificatesJob`.
8
+ attr_accessor :renewal_batch_size
9
+
10
+ # File name of the certbot executable.
11
+ attr_accessor :certbot_executable
12
+
13
+ # Port for the standalone certbot HTTP server
14
+ attr_accessor :certbot_port
15
+
16
+ # Directory certbot outputs certificates to
17
+ attr_accessor :certbot_output_directory
18
+
19
+ # Directory the web server reads pem files from
20
+ attr_accessor :pem_directory
21
+
22
+ # Command to make server reload pem files
23
+ attr_accessor :server_reload_command
24
+
25
+ def initialize
26
+ @renewal_interval = 1.month
27
+ @renewal_batch_size = 10
28
+
29
+ @certbot_executable = '/usr/local/share/letsencrypt/bin/certbot'
30
+ @certbot_port = 9999
31
+ @certbot_output_directory = '/etc/letsencrypt/live'
32
+
33
+ @pem_directory = '/etc/haproxy/ssl/'
34
+ @server_reload_command = '/etc/init.d/haproxy reload'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ module CertWatch
2
+ module DomainOwner
3
+ def self.define(options)
4
+ attribute = options.fetch(:attribute).to_s
5
+
6
+ Module.new do
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ after_save do
11
+ if changed.include?(attribute)
12
+ previous_value = changes[attribute].first
13
+ new_value = self[attribute]
14
+
15
+ Certificate.find_by(domain: previous_value).try(:abandon)
16
+ Certificate.find_or_create_by(domain: new_value).renew if new_value.present?
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ require 'state_machine'
2
+ require 'state_machine_job'
3
+
4
+ module CertWatch
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace CertWatch
7
+
8
+ config.autoload_paths << File.join(config.root, 'lib')
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module CertWatch
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module CertWatch
2
+ class InstallError < Error
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module CertWatch
2
+ class PemDirectoryInstaller < Installer
3
+ def install(_domain)
4
+ fail(NotImplementedError)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ module CertWatch
2
+ class PemDirectoryInstaller
3
+ def initialize(options)
4
+ @pem_directory = options.fetch(:pem_directory)
5
+ @input_directory = options.fetch(:input_directory)
6
+ @reload_command = options[:reload_command]
7
+ end
8
+
9
+ def install(domain)
10
+ if Rails.env.development?
11
+ Rails.logger.info("[CertWatch] Skipping certificate install for #{domain} in development.")
12
+ return
13
+ end
14
+
15
+ Sanitize.check_domain!(domain)
16
+
17
+ check_inputs_exist(domain)
18
+ write_pem_file(domain)
19
+ perform_reload_command
20
+ end
21
+
22
+ private
23
+
24
+ def check_inputs_exist(domain)
25
+ Shell.sudo("ls #{input_files(domain)}")
26
+ rescue Shell::CommandFailed
27
+ fail(InstallError, "Input files '#{input_files(domain)}' do not exist.")
28
+ end
29
+
30
+ def write_pem_file(domain)
31
+ sudo("cat #{input_files(domain)} > #{pem_file(domain)}")
32
+ end
33
+
34
+ def input_files(domain)
35
+ ['fullchain.pem', 'privkey.pem'].map do |file_name|
36
+ File.join(@input_directory, domain, file_name)
37
+ end.join(' ')
38
+ end
39
+
40
+ def pem_file(domain)
41
+ File.join(@pem_directory, "#{domain}.pem")
42
+ end
43
+
44
+ def perform_reload_command
45
+ return unless @reload_command
46
+ sudo(@reload_command)
47
+ end
48
+
49
+ def sudo(command)
50
+ Shell.sudo(command)
51
+ rescue Shell::CommandFailed => e
52
+ fail(InstallError, e.message)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,4 @@
1
+ module CertWatch
2
+ class RenewError < Error
3
+ end
4
+ end