symbiosis-ssl 6.03 KB
Newer Older
1
#!/usr/bin/ruby
2
#
3
# NAME
4
#   symbiosis-ssl - Manage and generate SSL certificates
5
6
#
# SYNOPSIS
Jamie Nguyen's avatar
Jamie Nguyen committed
7
8
9
#   symbiosis-ssl [ --threshold days ] [ --no-generate ] [ --no-rollover ] [ --select set ]
#     [ --list ] [ --prefix prefix ] [ --verbose ] [ --debug ] [ --manual ] [ --help ]
#     [ domain domain ... ]
10
11
#
# OPTIONS
12
#  --force          Re-generate certificates, and roll over to the new set even
13
#                   if they're not due to be renewed. Implies --verbose.
14
#
15
16
17
18
19
#  --threshold days  Number of days before expiry that certificates should be renewed. Defaults to 21.
#
#  --select set     Select a specific set for a single domain. A domain must be specified.
#
#  --list           List available SSL certificate sets for a domain.
20
#
21
22
23
24
#  --no-generate    Do not try and generate keys or certificates.
#
#  --no-rollover    Do not try and generate keys or certificates.
#
25
26
27
28
29
30
#  --prefix prefix  Set the directory prefix for Symbiosis. Defaults to /srv.
#
#  --help           Show the help information for this script.
#
#  --manual         Show the manual for this script
#
Jamie Nguyen's avatar
Jamie Nguyen committed
31
32
33
#  --verbose        Show verbose information.
#
#  --debug          Show debugging information.
34
35
36
37
38
39
#
# USAGE
#
# This command is used to manage certificate sets automatically for domains on
# a Symbiosis system. It can request certificates from LetsEncrypt or generate
# self-signed ones (see PROVIDERS).
40
#
41
# PROVIDERS
42
#
43
44
45
46
# Currently two providers are supported, namely LetsEncrypt and SelfSigned. A
# domain can be set up to use either provider by setting a file
# /srv/example.com/config/ssl-provider with the name of the desired provider in
# it.
47
#
48
49
50
# If the provider is set to something else (e.g. CertificateProviderDuJour)
# then no certificates will be generated, but it is possible to manage updating
# certificates with this program.
51
52
53
54
55
56
57
58
59
#
# AUTHOR
#   Patrick J. Cherry <patrick@bytemark.co.uk>
#

#
#  Modules we require
#

60
require 'English'
61
62
63
require 'getoptlong'

opts = GetoptLong.new(
64
65
66
67
68
69
70
71
72
73
  ['--help', '-h', GetoptLong::NO_ARGUMENT],
  ['--manual', '-m', GetoptLong::NO_ARGUMENT],
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
  ['--debug', '-d', GetoptLong::NO_ARGUMENT],
  ['--force', '-f', GetoptLong::NO_ARGUMENT],
  ['--list', '-l', GetoptLong::NO_ARGUMENT],
  ['--threshold', '-t', GetoptLong::REQUIRED_ARGUMENT],
  ['--no-generate', '-G', GetoptLong::NO_ARGUMENT],
  ['--no-rollover', '-R', GetoptLong::NO_ARGUMENT],
  ['--select', '-s', GetoptLong::REQUIRED_ARGUMENT],
telyn's avatar
telyn committed
74
75
  ['--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT],
  ['--root-dir', '-r', GetoptLong::REQUIRED_ARGUMENT]
76
77
78
79
)

manual = help = false
$VERBOSE = false
Jamie Nguyen's avatar
Jamie Nguyen committed
80
$DEBUG = false
81
prefix = '/srv'
82
83
do_list = do_generate = do_rollover = nil
rollover_to = nil
84
threshold = 21
telyn's avatar
telyn committed
85
root = '/'
86
87
88

opts.each do |opt,arg|
  case opt
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
  when '--no-generate'
    do_generate = false
  when '--no-rollover'
    do_rollover = false
  when '--select'
    rollover_to = arg.to_s
  when '--force'
    do_generate = do_rollover = true
    $VERBOSE = true
  when '--threshold'
    begin
      threshold = Integer(arg)
    rescue ArgumentError
      warn "** Could not parse #{arg.inspect} as an integer for --threshold"
    end
  when '--help'
    help = true
  when '--manual'
    manual = true
  when '--prefix'
    prefix = arg
telyn's avatar
telyn committed
110
111
  when '--root-dir'
    root = arg
112
113
114
115
116
117
  when '--list'
    do_list = true
  when '--verbose'
    $VERBOSE = true
  when '--debug'
    $DEBUG = true
118
119
120
  end
end

telyn's avatar
telyn committed
121
122
prefix == '/srv' && prefix = File.join(root, '/srv')

123
124
125
#
# Output help as required.
#
126
if help || manual
127
128
129
130
131
132
  require 'symbiosis/utils'
  Symbiosis::Utils.show_help(__FILE__) if help
  Symbiosis::Utils.show_manual(__FILE__) if manual
  exit 0
end

133
#
134
# The requires spawn a massive stack of warnings in verbose mode.  So let's
135
136
137
138
139
140
# hide them.
#
v = $VERBOSE
$VERBOSE = false

require 'symbiosis/domains'
141
142
require 'symbiosis/domain/ssl'
require 'symbiosis/ssl'
143
144
145
146
147
148
149
150
require 'symbiosis/ssl/letsencrypt'
require 'symbiosis/ssl/selfsigned'

#
# And unhide.  Ugh.
#
$VERBOSE = v

151
152
153
154
155
156
domains = []

ARGV.each do |arg|
  domain = Symbiosis::Domains.find(arg.to_s, prefix)

  if domain.nil?
157
    warn "** Unable to find/parse domain #{arg.inspect}"
158
159
160
161
162
163
    next
  end

  domains << domain
end

164
165
if rollover_to && ARGV.length != 1
  warn '** Exactly one domain must be specfied when rolling over to a specific set.'
166
167
168
  exit 1
end

169
domains = Symbiosis::Domains.all(prefix) if ARGV.empty?
170

171
exit_code = 0
172

173
%w[INT TERM].each do |sig|
174
  trap(sig) do
175
    if Process.uid.zero?
176
177
      Process.euid = 0
      Process.egid = 0
178
179
    end

180
    exit 1
181
  end
182
end
183

184
now = Time.now
185

telyn's avatar
telyn committed
186
187
domains_altered = []

188
189
domains.sort_by(&:name).each do |domain|
  if do_list || rollover_to
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
    puts "Certificate sets for #{domain}:"

    if domain.ssl_available_sets.empty?
      puts "\t** No sets found\n\n"
      next
    end

    domain.ssl_available_sets.each do |this_set|
      if this_set.certificate.issuer == this_set.certificate.subject
        puts "\tSSL set #{this_set.name}: self-signed for #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}"
      else
        puts "\tSSL set #{this_set.name}: signed by #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}"
      end
    end

    current = domain.ssl_current_set
    puts "\tCurrent SSL set: #{current.name}\n" unless $VERBOSE

208
    next if rollover_to.nil?
209

210
    to_set = domain.ssl_available_sets.find { |s| s.name.to_s == rollover_to }
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

    if to_set.nil?
      puts "\tThere is no set '#{rollover_to}' available for this domain."
      next
    end

    if to_set == current
      puts "\tNo need to change to set #{to_set.name} as this is already current."
      next
    end

    puts "\tRolling over from set #{current.name} to #{to_set.name}"
    domain.ssl_rollover(to_set)
    puts "\tCurrent SSL set now: #{domain.ssl_current_set.name}\n"
    next
  end

228
  begin
telyn's avatar
telyn committed
229
230
    rollover_performed = domain.ssl_magic(threshold, do_generate, do_rollover, now)
    domains_altered.push domain.name if rollover_performed
231
  rescue StandardError => err
232
    puts "\t!! Failed: #{err.to_s.gsub($RS, '')}" if $VERBOSE
233
234
    puts err.backtrace.join("\n") if $DEBUG
    exit_code = 1
235
236
237
  end
end

telyn's avatar
telyn committed
238
239
Symbiosis::SSL.call_hooks domains_altered

240
exit exit_code