summaryrefslogtreecommitdiff
path: root/code/environments/production/modules/certregen/lib/puppet/face/certregen.rb
blob: 24c4b30e7a43628db2911f0ad52dda4f8ca68443 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
require 'puppet/face'
require 'puppet_x/certregen/ca'
require 'puppet_x/certregen/certificate'
require 'puppet_x/certregen/crl'
require 'puppet/feature/chloride'

Puppet::Face.define(:certregen, '0.1.0') do
  copyright "Puppet", 2016
  summary "Regenerate the Puppet CA and client certificates"

  description <<-EOT
    This subcommand provides tools for monitoring the health of the Puppet CA, regenerating
    expiring CA certificates, and remediation for expired CA certificates.
  EOT

  action(:ca) do
    summary "Refresh the Puppet CA certificate and CRL"

    option('--ca_serial SERIAL') do
      summary 'The serial number (in hexadecimal) of the CA to rotate.'
    end

    when_invoked do |opts|
      cert = Puppet::Face[:certregen, :current].cacert(:ca_serial => opts[:ca_serial])
      crl = Puppet::Face[:certregen, :current].crl()
      [cert, crl]
    end

    when_rendering :console do |(cert, crl)|
      "CA expiration is now #{cert.content.not_after}\n" + \
      "CRL next update is now #{crl.content.next_update}"
    end
  end

  action(:cacert) do
    summary "Regenerate the Puppet CA certificate"

    description <<-EOT
      This subcommand generates a new CA certificate that can replace the existing CA certificate.
      The new CA certificate uses the same subject as the current CA certificate and reuses the
      key pair associated with the current CA certificate, so all certificates signed by the old
      CA certificate will remain valid.
    EOT

    option('--ca_serial SERIAL') do
      summary 'The serial number (in hexadecimal) of the CA to rotate.'
    end

    when_invoked do |opts|
      ca = PuppetX::Certregen::CA.setup

      current_ca_serial = ca.host.certificate.content.serial.to_s(16)
      if opts[:ca_serial].nil?
        raise "The serial number of the CA certificate to rotate must be provided. If you " \
          "are sure that you want to rotate the CA certificate, rerun this command with " \
          "--ca_serial #{current_ca_serial}"
      elsif opts[:ca_serial] != current_ca_serial
        raise "The serial number of the current CA certificate (#{current_ca_serial}) "\
          "does not match the serial number given on the command line (#{opts[:ca_serial]}). "\
          "If you are sure that you want to rotate the CA certificate, rerun this command with "\
          "--ca_serial #{current_ca_serial}"
      end

      PuppetX::Certregen::CA.backup
      PuppetX::Certregen::CA.regenerate(ca)
      Puppet::SSL::Certificate.indirection.find(Puppet::SSL::CA_NAME)
    end

    when_rendering(:console) do |cert|
      "CA expiration is now #{cert.content.not_after}"
    end
  end

  action(:crl) do
    summary 'Update the lastUpdate and nextUpdate field for the CA CRL'

    when_invoked do |opts|
      ca = PuppetX::Certregen::CA.setup
      PuppetX::Certregen::CRL.refresh(ca)
    end

    when_rendering(:console) do |crl|
      "CRL next update is now #{crl.content.next_update}"
    end
  end

  action(:healthcheck) do
    summary "Check for expiring certificates"

    description <<-EOT
      This subcommand checks for certificates that are nearing or past expiration.
    EOT

    option('--all') do
      summary "Report certificate expiry for all nodes, including nodes that aren't near expiration."
    end

    when_invoked do |opts|
      ca = PuppetX::Certregen::CA.setup

      certs = Puppet::SSL::Certificate.indirection.search('*').select do |cert|
        opts[:all] || PuppetX::Certregen::Certificate.expiring?(cert)
      end

      cacert = ca.host.certificate
      certs << cacert if (opts[:all] || PuppetX::Certregen::Certificate.expiring?(cacert))

      certs.sort { |a, b| a.content.not_after <=> b.content.not_after }
    end

    when_rendering :console do |certs|
      if certs.empty?
        "No certificates are approaching expiration."
      else
        certs.map do |cert|
          str = "#{cert.name.inspect} #{cert.digest.to_s}\n"
          expiry = PuppetX::Certregen::Certificate.expiry(cert)
          str << "Status: #{expiry[:status]}\n"
          str << "Expiration date: #{expiry[:expiration_date]}\n"
          if expiry[:expires_in]
            str << "Expires in: #{expiry[:expires_in]}\n"
          end
          str
        end
      end
    end

    when_rendering :pson do |certs|
      certs.map do |cert|
        {
          :name => cert.name,
          :digest => cert.digest.to_s,
          :expiry => PuppetX::Certregen::Certificate.expiry(cert)
        }
      end
    end

    when_rendering :yaml do |certs|
      certs.map do |cert|
        {
          :name => cert.name,
          :digest => cert.digest.to_s,
          :expiry => PuppetX::Certregen::Certificate.expiry(cert)
        }
      end
    end
  end

  action(:redistribute) do
    summary "Redistribute the regenerated CA certificate and CRL to nodes in PuppetDB"

    description <<-EOT
      Redistribute the regenerated CA certificate and CRL to active nodes in PuppetDB. This command is
      only necessary if the CA certificate is expired and a new CA certificate needs to be manually
      distributed via SSH.

      This subcommand depends on the `chloride` gem, which is not included with this Puppet face.

      Distributing the CA certificate via SSH requires either a private ssh key (given by the
      `--ssh_key_file` flag) or entering the password when prompted. If password auth is used,
      the `highline` gem should be installed so that the entered password is not echoed to the
      terminal.
    EOT

    option('--username USER') do
      summary "The username to use when logging into the remote machine"
    end

    option('--ssh_key_file FILE') do
      summary "The SSH key file to use for authentication"
      default_to { "~/.ssh/id_rsa" }
    end

    when_invoked do |opts|
      unless Puppet.features.chloride?
        raise "Unable to distribute CA certificate: the chloride gem is not available."
      end

      config = {}

      config.merge!(username: opts[:username]) if opts[:username]
      config.merge!(ssh_key_file: File.expand_path(opts[:ssh_key_file])) if opts[:ssh_key_file]

      ca = PuppetX::Certregen::CA.setup
      cacert = ca.host.certificate
      if PuppetX::Certregen::Certificate.expiring?(cacert)
        Puppet.err "Refusing to distribute CA certificate: certificate is pending expiration."
        exit 1
      end

      rv = {succeeded: [], failed: []}
      PuppetX::Certregen::CA.certnames.each do |certname|
        begin
          PuppetX::Certregen::CA.distribute(certname, config)
          rv[:succeeded] << certname
        rescue => e
          Puppet.log_exception(e)
          rv[:failed] << certname
        end
      end

      rv
    end
  end
end