symbiosis-ssl 6.68 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
42
43
# In addition, if any domain's certificate set was altered, hooks are run (see
# HOOKS).
#
44
# PROVIDERS
45
#
46
47
48
49
# 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.
50
#
51
52
53
# 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.
54
#
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# HOOKS
#
# Hooks are executed from the /etc/symbiosis/ssl-hooks.d directory, given the
# following conditions:
#
# * The file is executable
# * The file's name is made up only of alphanumerics, underscore (_) and hyphen
# (-)
#
# At present, only one event causes the hooks to be executed. If any domain's
# certificate set is altered by symbiosis-ssl, at the end of the process all
# the hooks are called with 'live-update' passed as their only command-line
# argument and the list of domains that were altered
#
69
70
71
72
73
74
75
76
# AUTHOR
#   Patrick J. Cherry <patrick@bytemark.co.uk>
#

#
#  Modules we require
#

77
require 'English'
78
79
80
require 'getoptlong'

opts = GetoptLong.new(
81
82
83
84
85
86
87
88
89
90
  ['--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
91
  ['--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT],
92
  ['--etc-dir', '-r', GetoptLong::REQUIRED_ARGUMENT]
93
94
95
96
)

manual = help = false
$VERBOSE = false
Jamie Nguyen's avatar
Jamie Nguyen committed
97
$DEBUG = false
98
prefix = '/srv'
99
100
do_list = do_generate = do_rollover = nil
rollover_to = nil
101
threshold = 21
102
etc_dir = '/etc'
103
104
105

opts.each do |opt,arg|
  case opt
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
  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
127
128
  when '--etc-dir'
    etc_dir = arg
129
130
131
132
133
134
  when '--list'
    do_list = true
  when '--verbose'
    $VERBOSE = true
  when '--debug'
    $DEBUG = true
135
136
137
  end
end

telyn's avatar
telyn committed
138

139
140
141
#
# Output help as required.
#
142
if help || manual
143
144
145
146
147
148
  require 'symbiosis/utils'
  Symbiosis::Utils.show_help(__FILE__) if help
  Symbiosis::Utils.show_manual(__FILE__) if manual
  exit 0
end

149
#
150
# The requires spawn a massive stack of warnings in verbose mode.  So let's
151
152
153
154
155
# hide them.
#
v = $VERBOSE
$VERBOSE = false

156
require 'symbiosis'
157
require 'symbiosis/domains'
158
159
require 'symbiosis/domain/ssl'
require 'symbiosis/ssl'
160
161
162
163
164
165
166
167
require 'symbiosis/ssl/letsencrypt'
require 'symbiosis/ssl/selfsigned'

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

168
169
Symbiosis.etc = etc_dir

170
171
172
173
174
175
domains = []

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

  if domain.nil?
176
    warn "** Unable to find/parse domain #{arg.inspect}"
177
178
179
180
181
182
    next
  end

  domains << domain
end

183
184
if rollover_to && ARGV.length != 1
  warn '** Exactly one domain must be specfied when rolling over to a specific set.'
185
186
187
  exit 1
end

188
domains = Symbiosis::Domains.all(prefix) if ARGV.empty?
189

190
exit_code = 0
191

192
%w[INT TERM].each do |sig|
193
  trap(sig) do
194
    if Process.uid.zero?
195
196
      Process.euid = 0
      Process.egid = 0
197
198
    end

199
    exit 1
200
  end
201
end
202

203
now = Time.now
204

telyn's avatar
telyn committed
205
206
domains_altered = []

207
208
domains.sort_by(&:name).each do |domain|
  if do_list || rollover_to
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    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

227
    next if rollover_to.nil?
228

229
    to_set = domain.ssl_available_sets.find { |s| s.name.to_s == rollover_to }
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246

    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

247
  begin
telyn's avatar
telyn committed
248
249
    rollover_performed = domain.ssl_magic(threshold, do_generate, do_rollover, now)
    domains_altered.push domain.name if rollover_performed
250
  rescue StandardError => err
251
    puts "\t!! Failed: #{err.to_s.gsub($RS, '')}" if $VERBOSE
252
253
    puts err.backtrace.join("\n") if $DEBUG
    exit_code = 1
254
255
256
  end
end

257
success = Symbiosis::SSL::Hooks.run! domains_altered, 'live-update'
258
exit_code = 2 unless success
telyn's avatar
telyn committed
259

260
exit exit_code