Skip to content

Commit 1a3c470

Browse files
Add run_local_ca.sh script
This will give us the opportunity to create debug certificates with the help of a local self-signed certificate authority, which won't even need an internet connection to work.
1 parent 9c4fb72 commit 1a3c470

File tree

2 files changed

+257
-4
lines changed

2 files changed

+257
-4
lines changed

src/scripts/run_local_ca.sh

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Important files necessary for this script to work. The LOCAL_CA_DIR variable
5+
# is read from the environment if it is set, else it will use the default
6+
# provided here.
7+
: ${LOCAL_CA_DIR:="/etc/local_ca"}
8+
LOCAL_CA_KEY="${LOCAL_CA_DIR}/caPrivkey.pem"
9+
LOCAL_CA_CRT="${LOCAL_CA_DIR}/caCert.pem"
10+
LOCAL_CA_DB="${LOCAL_CA_DIR}/index.txt"
11+
LOCAL_CA_SRL="${LOCAL_CA_DIR}/serial.txt"
12+
LOCAL_CA_CRT_DIR="${LOCAL_CA_DIR}/new_certs"
13+
14+
# Source in util.sh so we can have our nice tools.
15+
. "$(cd "$(dirname "$0")"; pwd)/util.sh"
16+
17+
info "Starting certificate renewal process with local CA"
18+
19+
# We require an email to be set here as well, in order to simulate how it would
20+
# be in the real certbot case.
21+
if [ -z "${CERTBOT_EMAIL}" ]; then
22+
error "CERTBOT_EMAIL environment variable undefined; local CA will do nothing!"
23+
exit 1
24+
fi
25+
26+
# Ensure that an RSA key size is set.
27+
if [ -z "${RSA_KEY_SIZE}" ]; then
28+
debug "RSA_KEY_SIZE unset, defaulting to 2048"
29+
RSA_KEY_SIZE=2048
30+
fi
31+
32+
# This is an OpenSSL configuration file that has settings for creating a well
33+
# configured CA, as well as server certificates that adhere to the strict
34+
# standards of web browsers. This is not complete, but will have the missing
35+
# sections dynamically assembled by the functions that need them at runtime.
36+
openssl_cnf="
37+
# This section is invoked when running 'openssl ca ...'
38+
[ ca ]
39+
default_ca = custom_ca_settings
40+
41+
[ custom_ca_settings ]
42+
private_key = ${LOCAL_CA_KEY}
43+
certificate = ${LOCAL_CA_CRT}
44+
database = ${LOCAL_CA_DB}
45+
serial = ${LOCAL_CA_SRL}
46+
new_certs_dir = ${LOCAL_CA_CRT_DIR}
47+
default_days = 30
48+
default_md = sha256
49+
email_in_dn = yes
50+
unique_subject = no
51+
policy = custom_ca_policy
52+
53+
[ custom_ca_policy ]
54+
countryName = optional
55+
stateOrProvinceName = optional
56+
localityName = optional
57+
organizationName = optional
58+
organizationalUnitName = optional
59+
commonName = supplied
60+
emailAddress = supplied
61+
62+
# This section is invoked when running 'openssl req ...'
63+
[ req ]
64+
default_md = sha256
65+
prompt = no
66+
utf8 = yes
67+
string_mask = utf8only
68+
distinguished_name = dn_section
69+
# ^-- This needs to be defined else 'req' will fail with:
70+
# openssl unable to find 'distinguished_name' in config
71+
# If the '[dn_section]' is defined, but empty, we instead get:
72+
# error, no objects specified in config file
73+
# This is true even if we create a fully valid '-subj' string while using
74+
# these commands. LibreSSL also prioritize this content over what is being
75+
# sent in via '-subj', which is opposite to how OpenSSL works. Solution is
76+
# to assemble this section with the help of printf when using this command.
77+
78+
# These extensions should be supplied when creating the CA certificate.
79+
[ ca_cert ]
80+
basicConstraints = critical, CA:true
81+
subjectKeyIdentifier = hash
82+
authorityKeyIdentifier = keyid:always,issuer:always
83+
keyUsage = critical, keyCertSign, cRLSign
84+
subjectAltName = email:copy
85+
issuerAltName = issuer:copy
86+
87+
# These extensions should be supplied when creating a server certificate.
88+
[ server_cert ]
89+
basicConstraints = critical, CA:false
90+
subjectKeyIdentifier = hash
91+
authorityKeyIdentifier = keyid,issuer
92+
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
93+
extendedKeyUsage = serverAuth, clientAuth
94+
issuerAltName = issuer:copy
95+
subjectAltName = @alt_names
96+
# ------------------------^
97+
# Alt names must include all domain names/IPs the server certificate should be
98+
# valid for. This will be populated by the script later.
99+
"
100+
101+
102+
# Helper function to create a private key and a self-signed certificate to be
103+
# used as our local certificate authority. If the files already exist it will
104+
# do nothing, which means that it is actually possible to host mount a
105+
# completely custom CA here if you want to.
106+
generate_ca() {
107+
# Make sure necessary folders are present.
108+
mkdir -vp "${LOCAL_CA_DIR}"
109+
mkdir -vp "${LOCAL_CA_CRT_DIR}"
110+
111+
# Make sure there is a private key available for the CA.
112+
if [ ! -f "${LOCAL_CA_KEY}" ]; then
113+
info "Generating new private key for local CA"
114+
openssl genrsa -out "${LOCAL_CA_KEY}" "${RSA_KEY_SIZE}"
115+
fi
116+
117+
# Make sure there exists a self-signed certificate for the CA.
118+
if [ ! -f "${LOCAL_CA_CRT}" ]; then
119+
info "Creating new self-signed certificate for local CA"
120+
openssl req -x509 -new -nodes \
121+
-config <(printf "%s\n" \
122+
"${openssl_cnf}" \
123+
"[ dn_section ]" \
124+
"countryName = SE" \
125+
"0.organizationName = github.com/JonasAlfredsson" \
126+
"organizationalUnitName = docker-nginx-certbot" \
127+
"commonName = Local Debug CA" \
128+
"emailAddress = ${CERTBOT_EMAIL}" \
129+
) \
130+
-extensions ca_cert \
131+
-days 30 \
132+
-key "${LOCAL_CA_KEY}" \
133+
-out "${LOCAL_CA_CRT}"
134+
fi
135+
136+
# If a serial file does not exist, or if it has a size of zero, we create
137+
# one with an initial value.
138+
if [ ! -f "${LOCAL_CA_SRL}" ] || [ ! -s "${LOCAL_CA_SRL}" ]; then
139+
info "Creating new serial file for local CA"
140+
openssl rand -hex 20 > "${LOCAL_CA_SRL}"
141+
fi
142+
143+
# Make sure there is a database file.
144+
if [ ! -f "${LOCAL_CA_DB}" ]; then
145+
info "Creating new index file for local CA"
146+
touch "${LOCAL_CA_DB}"
147+
fi
148+
}
149+
150+
# Helper function that use the local CA in order to create a valid signed
151+
# certificate for the given cert name.
152+
#
153+
# $1: The name of the certificate (e.g. domain)
154+
# $@: All alternate name variants, separated by space
155+
# (e.g. DNS.1=domain.org DNS.2=localhost IP.1=127.0.0.1)
156+
get_certificate() {
157+
# Store the cert name for future use, and then `shift` so the rest of the
158+
# input arguments are just alt names.
159+
local cert_name="$1"
160+
shift
161+
162+
# Make sure the necessary folder exists.
163+
mkdir -vp "/etc/letsencrypt/live/${cert_name}"
164+
165+
# Make sure there is a private key available for the domain in question.
166+
# It is good practice to generate a new key every time a new certificate is
167+
# requested, in order to guard against potential key compromises.
168+
info "Generating new private key for '${cert_name}'"
169+
openssl genrsa -out "/etc/letsencrypt/live/${cert_name}/privkey.pem" "${RSA_KEY_SIZE}"
170+
171+
# Create a certificate signing request from the private key.
172+
info "Generating certificate signing request for '${cert_name}'"
173+
openssl req -new -config <(printf "%s\n" \
174+
"${openssl_cnf}" \
175+
"[ dn_section ]" \
176+
"commonName = ${cert_name}" \
177+
"emailAddress = ${CERTBOT_EMAIL}" \
178+
) \
179+
-key "/etc/letsencrypt/live/${cert_name}/privkey.pem" \
180+
-out "${LOCAL_CA_DIR}/${cert_name}.csr"
181+
182+
# Sign the certificate with all the alternative names appended to the
183+
# appropriate section of the config file.
184+
info "Using local CA to sign certificate for '${cert_name}'"
185+
openssl ca -batch -notext \
186+
-config <(printf "%s\n" \
187+
"${openssl_cnf}" \
188+
"[alt_names]" \
189+
"$@" \
190+
) \
191+
-extensions server_cert \
192+
-in "${LOCAL_CA_DIR}/${cert_name}.csr" \
193+
-out "/etc/letsencrypt/live/${cert_name}/cert.pem"
194+
195+
# Create the other two files necessary to match what certbot produces.
196+
cp "${LOCAL_CA_CRT}" "/etc/letsencrypt/live/${cert_name}/chain.pem"
197+
cat "/etc/letsencrypt/live/${cert_name}/cert.pem" > "/etc/letsencrypt/live/${cert_name}/fullchain.pem"
198+
cat "/etc/letsencrypt/live/${cert_name}/chain.pem" >> "/etc/letsencrypt/live/${cert_name}/fullchain.pem"
199+
200+
# Cleanup after ourselves.
201+
rm "${LOCAL_CA_DIR}/${cert_name}.csr"
202+
}
203+
204+
# Begin with making sure that we have all the files necessary for a local CA.
205+
# This is really cheap to do, so I think it is fine that we check this every
206+
# time this script is invoked.
207+
generate_ca
208+
209+
# Go through all .conf files and find all cert names for which we should create
210+
# certificate requests and have them signed.
211+
for conf_file in /etc/nginx/conf.d/*.conf*; do
212+
for cert_name in $(parse_cert_names "${conf_file}"); do
213+
# Find all 'server_names' in this .conf file and assemble the list of
214+
# domains to be included in the request.
215+
ip_count=0
216+
dns_count=0
217+
alt_names=()
218+
for server_name in $(parse_server_names "${conf_file}"); do
219+
if [[ "${server_name}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
220+
# See if the alt name looks like an IPv4 address.
221+
ip_count=$((${ip_count} + 1))
222+
alt_names+=("IP.${ip_count}=${server_name}")
223+
elif [[ "${server_name,,}" =~ ^([a-f0-9]{1,4})?:([a-f0-9:]*):.*?$ ]]; then
224+
# This is a dirty check to see if it looks like an IPv6 address,
225+
# can easily be fooled but works for us right now.
226+
ip_count=$((${ip_count} + 1))
227+
alt_names+=("IP.${ip_count}=${server_name}")
228+
else
229+
# Else we suppose this is a valid DNS name.
230+
dns_count=$((${dns_count} + 1))
231+
alt_names+=("DNS.${dns_count}=${server_name}")
232+
fi
233+
done
234+
235+
# Hand over all the info required for the certificate request, and
236+
# let the local CA handle the rest.
237+
if ! get_certificate "${cert_name}" "${alt_names[@]}"; then
238+
error "Local CA failed for '${cert_name}'. Check the logs for details."
239+
fi
240+
done
241+
done
242+
243+
# After trying to sign all of the certificates, auto enable any configs that we
244+
# did indeed succeed with.
245+
auto_enable_configs
246+
247+
# Finally, tell Nginx to reload the configs.
248+
nginx -s reload

src/scripts/start_nginx_certbot.sh

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ else
4141
NGINX_PID=$!
4242
fi
4343

44-
info "Starting the certbot autorenewal service"
44+
info "Starting the autorenewal service"
4545
# Make sure a renewal interval is set before continuing.
4646
if [ -z "${RENEWAL_INTERVAL}" ]; then
4747
debug "RENEWAL_INTERVAL unset, using default of '8d'"
@@ -59,14 +59,19 @@ while [ true ]; do
5959
# Check that all dhparam files exists.
6060
"$(cd "$(dirname "$0")"; pwd)/create_dhparams.sh"
6161

62-
# Run certbot to check if any certificates needs renewal.
63-
"$(cd "$(dirname "$0")"; pwd)/run_certbot.sh"
62+
if [ 1 = ${USE_LOCAL_CA} ]; then
63+
# Renew all certificates with the help of the local CA.
64+
"$(cd "$(dirname "$0")"; pwd)/run_local_ca.sh"
65+
else
66+
# Run certbot to check if any certificates needs renewal.
67+
"$(cd "$(dirname "$0")"; pwd)/run_certbot.sh"
68+
fi
6469

6570
# Finally we sleep for the defined time interval before checking the
6671
# certificates again.
6772
# The "if" statement afterwards is to enable us to terminate this sleep
6873
# process (via the HUP trap) without tripping the "set -e" setting.
69-
info "Certbot autorenewal service will now sleep ${RENEWAL_INTERVAL}"
74+
info "Autorenewal service will now sleep ${RENEWAL_INTERVAL}"
7075
sleep "${RENEWAL_INTERVAL}" || x=$?; if [ -n "${x}" ] && [ "${x}" -ne "143" ]; then exit "${x}"; fi
7176
done
7277
) &

0 commit comments

Comments
 (0)