symbiosis-ssl 5.85 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
60
61
62
63
64
65
#
# AUTHOR
#   Patrick J. Cherry <patrick@bytemark.co.uk>
#

#
#  Modules we require
#

require 'getoptlong'

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

manual = help = false
$VERBOSE = false
Jamie Nguyen's avatar
Jamie Nguyen committed
78
$DEBUG = false
79
prefix = "/srv"
80
81
do_list = do_generate = do_rollover = nil
rollover_to = nil
82
threshold = 21
83
84
85

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

#
# Output help as required.
#
if help or manual
  require 'symbiosis/utils'
  Symbiosis::Utils.show_help(__FILE__) if help
  Symbiosis::Utils.show_manual(__FILE__) if manual
  exit 0
end

126
127
128
129
130
131
132
133
#
# The required spawn a massive stack of warnings in verbose mode.  So let's
# hide them.
#
v = $VERBOSE
$VERBOSE = false

require 'symbiosis/domains'
134
135
require 'symbiosis/domain/ssl'
require 'symbiosis/ssl'
136
137
138
139
140
141
142
143
require 'symbiosis/ssl/letsencrypt'
require 'symbiosis/ssl/selfsigned'

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

144
145
146
147
148
149
150

domains = []

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

  if domain.nil?
151
    warn "** Unable to find/parse domain #{arg.inspect}"
152
153
154
155
156
157
    next
  end

  domains << domain
end

158
159
160
161
162
if rollover_to and ARGV.length != 1
  warn "** Exactly one domain must be specfied when rolling over to a specific set."
  exit 1
end

163
164
165
166
if ARGV.empty?
  domains = Symbiosis::Domains.all(prefix)
end

167
exit_code = 0
168

169
%w(INT TERM).each do |sig|
170
  trap(sig) do
171

172
173
174
    if 0 == Process.uid
      Process.euid = 0
      Process.egid = 0
175
176
    end

177
    exit 1
178
  end
179
end
180

181
now = Time.now
182

183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
domains.sort{|a,b| a.name <=> b.name}.each do |domain|

  if do_list or rollover_to
    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

    if rollover_to.nil?
      next
    end

    to_set  = domain.ssl_available_sets.find{|s| s.name.to_s == rollover_to}

    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

226
  begin
227
    domain.ssl_magic(threshold, do_generate, do_rollover, now)
228
229
230
231
  rescue StandardError => err
    puts "\t!! Failed: #{err.to_s.gsub($/,'')}" if $VERBOSE
    puts err.backtrace.join("\n") if $DEBUG
    exit_code = 1
232
  end
233

234
235
end

236
237
exit exit_code