11#! /bin/bash
22
3- # Credit: https://github.com/Sharpie
3+ # Puppet CA extension script
4+ #
5+ # This script uses the Puppet CA certificates and private
6+ # keys to generate new CA certificates with extended 15 year
7+ # lifespans.
8+ #
9+ # This script operates on 2-certificate CA bundles used by
10+ # Puppet 6 and later in addition to single-certificate
11+ # bundles used by Puppet 5 and earlier.
12+ #
13+ # This script requires that the default locations for
14+ # the Puppet cadir and files underneath it are in use.
15+ #
16+ # Externally issued CA certificates are not supported.
417
518set -e
619
720PUPPET_BIN=' /opt/puppetlabs/puppet/bin'
821
9- ca_cert=$( " ${PUPPET_BIN} /puppet" config print --section master cacert)
10- ca_key=$( " ${PUPPET_BIN} /puppet" config print --section master cakey)
11- ca_dir=$( dirname " ${ca_cert} " )
22+ ca_bundle=$( " ${PUPPET_BIN} /puppet" config print --section master cacert)
23+ ca_dir=$( dirname " ${ca_bundle} " )
1224
13- printf ' CA certificate file: %s\n' " ${ca_cert} " >&2
14- printf ' CA private key file: %s\n' " ${ca_key} " >&2
25+ printf ' CA bundle file: %s\n' " ${ca_bundle} " >&2
1526
16- printf ' \nChecking CA chain length...\n' >&2
17- chain_length=$( grep -cF ' BEGIN CERTIFICATE' " ${ca_cert } " )
27+ printf ' \n Checking CA bundle length...\n' >&2
28+ chain_length=$( grep -cF ' BEGIN CERTIFICATE' " ${ca_bundle } " )
1829
19- if (( chain_length > 1 )) ; then
20- printf ' %s certificates were found in: %s\n' " ${chain_length} " " ${ca_cert} " >&2
21- printf ' This script only works on CA files that contain a single certificate.\n' >&2
22- exit 1
23- elif (( chain_length != 1 )) ; then
24- printf ' No certificates found in: %s\n' " ${ca_cert} " >&2
30+ if (( chain_length > 2 )) ; then
31+ printf ' %s certificates were found in: %s\n' " ${chain_length} " " ${ca_bundle} " >&2
32+ printf ' This script only works on CA bundles that contain one or two certificates.\n' >&2
2533 exit 1
34+ elif (( chain_length == 2 )) ; then
35+ printf ' 2 entry Puppet CA detected in: %s\n' " ${ca_bundle} " >&2
36+ root_key=" ${ca_dir} /root_key.pem"
37+ intermediate_key=" ${ca_dir} /ca_key.pem"
38+
39+ [[ -r " ${root_key} " ]] || {
40+ printf ' ERROR: The Root CA key file is not readable: %s\n' " ${root_key} " >&2
41+ printf ' This script must be run as root and does not support externally issued CA certs.\n' >&2
42+ exit 1
43+ }
44+
45+ [[ -r " ${intermediate_key} " ]] || {
46+ printf ' ERROR: The Intermediate CA key file is not readable: %s\n' " ${root_key} " >&2
47+ exit 1
48+ }
49+ elif (( chain_length == 1 )) ; then
50+ printf ' 1 entry Puppet CA detected in: %s\n' " ${ca_bundle} " >&2
51+ root_key=" ${ca_dir} /ca_key.pem"
52+
53+ [[ -r " ${root_key} " ]] || {
54+ printf ' ERROR: The Root CA key file is not readable: %s\n' " ${root_key} " >&2
55+ printf ' This script must be run as root and does not support externally issued CA certs.\n' >&2
56+ exit 1
57+ }
2658else
27- printf ' %s certificate found in: %s\n' " ${chain_length} " " ${ca_cert} " >&2
59+ printf ' ERROR: No certificates detected in: %s\n' " ${ca_bundle} " >&2
60+ exit 1
2861fi
2962
30- # Compute start and end dates for new certificate.
31- # Formats the year as YY instead of YYYY because the latter isn't supported
32- # until OpenSSL 1.1.1.
33- start_date=$( date -u --date=' -24 hours' ' +%y%m%d%H%M%SZ' )
34- end_date=$( date -u --date=' +15 years' ' +%y%m%d%H%M%SZ' )
35- ca_serial_num=$( " ${PUPPET_BIN} /openssl" x509 -in " ${ca_cert} " -noout -serial| cut -d= -f2)
3663
3764# Build a temporary directory with files required to renew the CA cert.
38- workdir=$( mktemp -d -t renew_ca_cert.XXX)
39- printf ' %s' " ${ca_serial_num} " > " ${workdir} /serial"
65+
66+ workdir=$( mktemp -d -t puppet_ca_extend.XXX)
67+ printf ' Using working directory: %s\n' " ${workdir} " >&2
68+
4069touch " ${workdir} /inventory"
4170touch " ${workdir} /inventory.attr"
42-
4371cat << EOT > "${workdir} /openssl.cnf"
4472[ca]
4573default_ca=ca_settings
@@ -59,27 +87,150 @@ commonName=supplied
5987basicConstraints=critical,CA:TRUE
6088keyUsage=keyCertSign,cRLSign
6189subjectKeyIdentifier=hash
62- authorityKeyIdentifier=issuer :always
90+ authorityKeyIdentifier=keyid :always
6391EOT
6492
93+ # Separate CA bundle out into individual certificates
94+ csplit -szf " ${workdir} /puppet-ca-cert-" " ${ca_bundle} " ' /-----BEGIN CERTIFICATE-----/' ' {*}'
95+ ca_certs=(" ${workdir} " /puppet-ca-cert-* )
96+
97+
98+ # Match keys up with certificates
99+ root_cert=' '
100+
101+ root_fingerprint=$( " ${PUPPET_BIN} /openssl" rsa -in " ${root_key} " -noout -modulus| cut -d= -f2-)
102+ for ca_cert in " ${ca_certs[@]} " ; do
103+ ca_fingerprint=$( " ${PUPPET_BIN} /openssl" x509 -in " ${ca_cert} " -noout -modulus| cut -d= -f2-)
104+ if [[ " ${ca_fingerprint} " == " ${root_fingerprint} " ]]; then
105+ root_cert=" ${ca_cert} "
106+ break
107+ fi
108+ done
109+
110+ [[ -n " ${root_cert} " ]] || {
111+ printf ' ERROR: Could not find a certificate matching key %s\n' " ${root_key} " >&2
112+ printf ' Checked: %s\n\t%s\n' " ${ca_certs[@]} " >&2
113+
114+ exit 1
115+ }
116+
117+ if (( chain_length == 2 )) ; then
118+ intermediate_cert=' '
119+
120+ intermediate_fingerprint=$( " ${PUPPET_BIN} /openssl" rsa -in " ${intermediate_key} " -noout -modulus| cut -d= -f2-)
121+ for ca_cert in " ${ca_certs[@]} " ; do
122+ ca_fingerprint=$( " ${PUPPET_BIN} /openssl" x509 -in " ${ca_cert} " -noout -modulus| cut -d= -f2-)
123+ if [[ " ${ca_fingerprint} " == " ${intermediate_fingerprint} " ]]; then
124+ intermediate_cert=" ${ca_cert} "
125+ break
126+ fi
127+ done
128+
129+ [[ -n " ${intermediate_cert} " ]] || {
130+ printf ' ERROR: Could not find a certificate matching key %s\n' " ${intermediate_key} " >&2
131+ printf ' Checked: %s\n\t%s\n' " ${ca_certs[@]} " >&2
132+
133+ exit 1
134+ }
135+ fi
136+
137+
138+ # Extend CA certs
139+
140+ # Compute start and end dates for new certificates.
141+ # Formats the year as YY instead of YYYY because the latter isn't supported
142+ # until OpenSSL 1.1.1.
143+ start_date=$( date -u --date=' -24 hours' ' +%y%m%d%H%M%SZ' )
144+ end_date=$( date -u --date=' +15 years' ' +%y%m%d%H%M%SZ' )
145+
146+ root_subject=$( " ${PUPPET_BIN} /openssl" x509 -in " ${root_cert} " -noout -subject| cut -d= -f2-)
147+ root_issuer=$( " ${PUPPET_BIN} /openssl" x509 -in " ${root_cert} " -noout -issuer| cut -d= -f2-)
148+ root_enddate=$( " ${PUPPET_BIN} /openssl" x509 -in " ${root_cert} " -noout -enddate| cut -d= -f2-)
149+ root_serial_num=$( " ${PUPPET_BIN} /openssl" x509 -in " ${root_cert} " -noout -serial| cut -d= -f2-)
150+
151+ [[ " ${root_subject} " = " ${root_issuer} " ]] || {
152+ printf ' ERROR: Root CA cert is not self-signed: %s\n' " ${root_cert} " >&2
153+ printf ' Subject: %s\n' " ${root_subject} " >&2
154+ printf ' Issuer: %s\n' " ${root_issuer} " >&2
155+ printf ' This script does not support externally-issued CAs.' >&2
156+
157+ exit 1
158+ }
159+
160+ printf ' \nExtending: %s\n' " ${root_cert} " >&2
161+ printf ' Subject: %s\n' " ${root_subject} " >&2
162+ printf ' Issuer: %s\n' " ${root_issuer} " >&2
163+ printf ' Serial: %s\n' " ${root_serial_num} " >&2
164+ printf ' End-Date: %s\n' " ${root_enddate} " >&2
165+
65166# Generate a signing request from the existing certificate
66167" ${PUPPET_BIN} /openssl" x509 -x509toreq \
67- -in " ${ca_cert } " \
68- -signkey " ${ca_key } " \
69- -out " ${workdir} /ca_csr .pem"
168+ -in " ${root_cert } " \
169+ -signkey " ${root_key } " \
170+ -out " ${workdir} /root_ca.csr .pem"
70171
71- # Sign the request
72- new_ca_cert=" ${ca_dir} /ca_crt-expires-${end_date} .pem"
172+ printf ' %s' " ${root_serial_num} " > " ${workdir} /serial"
73173
74174yes | " ${PUPPET_BIN} /openssl" ca \
75175 -notext \
76- -in " ${workdir} /ca_csr .pem" \
77- -keyfile " ${ca_key } " \
176+ -in " ${workdir} /root_ca.csr .pem" \
177+ -keyfile " ${root_key } " \
78178 -config " ${workdir} /openssl.cnf" \
79179 -selfsign \
80180 -startdate " ${start_date} " \
81181 -enddate " ${end_date} " \
82- -out " ${new_ca_cert} " >&2
182+ -out " ${workdir} /root_ca.renewed.pem" >&2
183+
184+ if (( chain_length == 2 )) ; then
185+ intermediate_subject=$( " ${PUPPET_BIN} /openssl" x509 -in " ${intermediate_cert} " -noout -subject| cut -d= -f2-)
186+ intermediate_issuer=$( " ${PUPPET_BIN} /openssl" x509 -in " ${intermediate_cert} " -noout -issuer| cut -d= -f2-)
187+ intermediate_enddate=$( " ${PUPPET_BIN} /openssl" x509 -in " ${intermediate_cert} " -noout -enddate| cut -d= -f2-)
188+ intermediate_serial_num=$( " ${PUPPET_BIN} /openssl" x509 -in " ${intermediate_cert} " -noout -serial| cut -d= -f2-)
189+
190+ [[ " ${intermediate_issuer} " == " ${root_issuer} " ]] || {
191+ printf ' ERROR: Intermediate CA cert is not issued by Root CA: %s\n' " ${intermediate_cert} " >&2
192+ printf ' Subject: %s\n' " ${intermediate_subject} " >&2
193+ printf ' Issuer: %s\n' " ${intermediate_issuer} " >&2
194+ printf ' This script does not support externally-issued CAs.' >&2
195+
196+ exit 1
197+ }
198+
199+ printf ' \nExtending: %s\n' " ${intermediate_cert} " >&2
200+ printf ' Subject: %s\n' " ${intermediate_subject} " >&2
201+ printf ' Issuer: %s\n' " ${intermediate_issuer} " >&2
202+ printf ' Serial: %s\n' " ${intermediate_serial_num} " >&2
203+ printf ' End-Date: %s\n' " ${intermediate_enddate} " >&2
204+
205+ # Generate a signing request from the existing certificate
206+ " ${PUPPET_BIN} /openssl" x509 -x509toreq \
207+ -in " ${intermediate_cert} " \
208+ -signkey " ${intermediate_key} " \
209+ -out " ${workdir} /intermediate_ca.csr.pem"
210+
211+ printf ' %s' " ${intermediate_serial_num} " > " ${workdir} /serial"
212+
213+ yes | " ${PUPPET_BIN} /openssl" ca \
214+ -notext \
215+ -in " ${workdir} /intermediate_ca.csr.pem" \
216+ -cert " ${workdir} /root_ca.renewed.pem" \
217+ -keyfile " ${root_key} " \
218+ -config " ${workdir} /openssl.cnf" \
219+ -startdate " ${start_date} " \
220+ -enddate " ${end_date} " \
221+ -out " ${workdir} /intermediate_ca.renewed.pem" >&2
222+ fi
223+
224+
225+ # Generate output bundle
226+ new_ca_bundle=" ${ca_dir} /ca_crt-expires-${end_date} .pem"
227+
228+ if (( chain_length == 2 )) ; then
229+ cat " ${workdir} /intermediate_ca.renewed.pem" \
230+ " ${workdir} /root_ca.renewed.pem" > " ${new_ca_bundle} "
231+ else
232+ cat " ${workdir} /root_ca.renewed.pem" > " ${new_ca_bundle} "
233+ fi
83234
84- printf ' \nRenewed CA certificate \n' >&2
85- printf ' %s\n' " ${new_ca_cert } "
235+ printf ' \nRenewed CA certificates. \n' >&2
236+ printf ' %s\n' " ${new_ca_bundle } "
0 commit comments