cert_watch 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +151 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/cert_watch/application.js +13 -0
- data/app/assets/stylesheets/cert_watch/application.css +15 -0
- data/app/controllers/cert_watch/application_controller.rb +5 -0
- data/app/jobs/cert_watch/install_certificate_job.rb +16 -0
- data/app/jobs/cert_watch/renew_certificate_job.rb +16 -0
- data/app/jobs/cert_watch/renew_expiring_certificates_job.rb +13 -0
- data/app/models/cert_watch/certificate.rb +42 -0
- data/app/views/layouts/cert_watch/application.html.erb +14 -0
- data/config/locales/de.yml +47 -0
- data/config/locales/en.yml +47 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20160711193700_create_certificates.rb +18 -0
- data/lib/cert_watch/CHANGELOG.md +5 -0
- data/lib/cert_watch/certbot_client.rb +32 -0
- data/lib/cert_watch/client.rb +7 -0
- data/lib/cert_watch/configuration.rb +37 -0
- data/lib/cert_watch/domain_owner.rb +23 -0
- data/lib/cert_watch/engine.rb +10 -0
- data/lib/cert_watch/error.rb +4 -0
- data/lib/cert_watch/install_error.rb +4 -0
- data/lib/cert_watch/installer.rb +7 -0
- data/lib/cert_watch/pem_directory_installer.rb +55 -0
- data/lib/cert_watch/renew_error.rb +4 -0
- data/lib/cert_watch/sanitize.rb +13 -0
- data/lib/cert_watch/shell.rb +20 -0
- data/lib/cert_watch/version.rb +3 -0
- data/lib/cert_watch/views/all.rb +3 -0
- data/lib/cert_watch/views/certificate_state.rb +42 -0
- data/lib/cert_watch.rb +32 -0
- data/spec/cert_watch/certbot_client_spec.rb +53 -0
- data/spec/cert_watch/domain_owner_spec.rb +62 -0
- data/spec/cert_watch/pem_directory_installer_spec.rb +75 -0
- data/spec/cert_watch/sanitize_spec.rb +19 -0
- data/spec/cert_watch/shell_spec.rb +19 -0
- data/spec/cert_watch/views/certificate_state_spec.rb +33 -0
- data/spec/examples.txt +40 -0
- data/spec/factories/certificates.rb +6 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +7 -0
- data/spec/internal/log/jobs/test/cert_watch.log +3793 -0
- data/spec/internal/log/test.log +24187 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb +43 -0
- data/spec/models/cert_watch/certificate_spec.rb +102 -0
- data/spec/rails_helper.rb +45 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/config/cert_watch.rb +7 -0
- data/spec/support/config/factory_girl.rb +11 -0
- data/spec/support/config/resque_logger.rb +16 -0
- data/spec/support/config/timecop.rb +21 -0
- data/spec/support/helpers/doubles.rb +28 -0
- data/spec/support/helpers/fixtures.rb +25 -0
- data/spec/support/helpers/inline_resque.rb +9 -0
- data/spec/support/helpers/view_component_example_group.rb +31 -0
- 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,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,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,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,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,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,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
|