cert_watch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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