Skip to content

Commit 034ccfb

Browse files
authored
Merge pull request #28 from WadeBarnes/feature/cert-per-host
Add support for managing a certificate per host
2 parents 44866f1 + 8efbdbd commit 034ccfb

5 files changed

Lines changed: 166 additions & 86 deletions

File tree

charts/certbot/templates/_helpers.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ template:
122122
value: {{ .Values.certbot.staging | quote }}
123123
- name: CERTBOT_SUBSET
124124
value: {{ .Values.certbot.subset | quote }}
125+
- name: CERTBOT_CERT_PER_HOST
126+
value: {{ .Values.certbot.certPerHost | quote }}
125127
resources:
126128
requests:
127129
cpu: 50m

charts/certbot/values.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ certbot:
3030
# secretKey: acme-server-url
3131
server: https://acme-v02.api.letsencrypt.org/directory
3232

33-
# Allow domain validation to pass if a subset of them are vali
33+
# Allow domain validation to pass if a subset of them are valid
3434
subset: true
3535

36+
# Manage an individual certificate per unique managed host (domain name), if true,
37+
# otherwise, manage a single certificate for all managed hosts (domain names)
38+
certPerHost: false
39+
3640
cron:
3741
# Every Monday & Thursday - https://crontab.guru/#0_0_*_*_1,4
3842
schedule: 0 0 * * 1,4

docker/README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ To learn more about the **Common Services** available visit the [Common Services
1010

1111
## Table of Contents
1212

13-
- [Summary](#summary)
14-
- [Environment Variables](#environment-variables)
15-
- [Quick Start](#quick-start)
16-
- [Manual Run](#manual-run)
17-
- [Cleanup](#cleanup)
18-
- [Entrust Usage](#entrust-usage)
19-
- [Tips](#tips)
20-
- [Appendix](#appendix)
21-
- [References](#references)
22-
- [Errata](#errata)
23-
- [License](#license)
13+
- [Summary](#summary)
14+
- [Environment Variables](#environment-variables)
15+
- [Quick Start](#quick-start)
16+
- [Manual Run](#manual-run)
17+
- [Cleanup](#cleanup)
18+
- [Entrust Usage](#entrust-usage)
19+
- [Tips](#tips)
20+
- [Appendix](#appendix)
21+
- [References](#references)
22+
- [Errata](#errata)
23+
- [License](#license)
2424

2525
## Summary
2626

@@ -29,7 +29,7 @@ To learn more about the **Common Services** available visit the [Common Services
2929
- Should only be executed on Openshift Container Platform
3030
- Creates an OpenShift `CronJob` which will run on a regular schedule for renewing TLS certificates
3131
- The `CronJob` will manage all `Route` objects annotated with the label `certbot-managed=true`
32-
- One certificate will be issued/renewed for all the managed hosts/domains
32+
- You have the option of a single certificate being issued/renewed for all the managed hosts/domains, or of an individual certificate being issued/renewed for each managed host/domain.
3333
- If a cert is created/renewed, patch the new certificate to the managed OpenShift routes
3434

3535
## Environment Variables
@@ -49,6 +49,7 @@ The Certbot container image supports an array of environment variables to config
4949
| `CERTBOT_RSA_KEY_SIZE` | `2048` | Key length for RSA keypair generation |
5050
| `CERTBOT_STAGING` | `false` | Use self-signed cert renewals. Must be `false` if using [Entrust](#entrust-usage)) |
5151
| `CERTBOT_SUBSET` | `true` | Allow Certbot to pass ACME challenge if at least one domain succeeds |
52+
| `CERTBOT_CERT_PER_HOST` | `false` | Manage an individual certificate per unique managed host (domain name), if true, otherwise, manage a single certificate for all managed hosts (domain names) |
5253

5354
## Quick Start
5455

@@ -81,6 +82,7 @@ The following provides you a quick way to get Certbot set up and running as an O
8182
| `CERTBOT_SERVER` | `https://acme-v02.api.letsencrypt.org/directory` | ACME Certbot endpoint. For BC Gov SSL, see [Entrust](#entrust-usage). |
8283
| `CERTBOT_STAGING` | `false` | Use self-signed cert renewals. Must be `false` if using [Entrust](#entrust-usage)) |
8384
| `CERTBOT_SUBSET` | `true` | Allow domain validation to pass if a subset of them are valid |
85+
| `CERTBOT_CERT_PER_HOST` | `false` | Manage an individual certificate per unique managed host (domain name), if true, otherwise, manage a single certificate for all managed hosts (domain names) |
8486
| `CRON_SCHEDULE` | `0 0 * * 1,4` | [Cronjob](https://crontab.guru) Schedule |
8587
| `CRON_SUSPEND` | `false` | Suspend cronjob |
8688
| `IMAGE_REGISTRY` | `docker.io` | Image Registry |

docker/entrypoint.sh

Lines changed: 138 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#!/bin/bash
22

33
: "${CERTBOT_CONFIG_DIR:=/etc/letsencrypt}"
4-
: "${CERTBOT_DEPLOY_DIR:=/etc/letsencrypt/renewal-hooks/deploy}"
4+
: "${CERTBOT_RENEWAL_DIR:=${CERTBOT_CONFIG_DIR}/renewal}"
5+
: "${CERTBOT_DEPLOY_DIR:=${CERTBOT_CONFIG_DIR}/renewal-hooks/deploy}"
56
: "${CERTBOT_LOGS_DIR:=/var/log/letsencrypt}"
67
: "${CERTBOT_WORK_DIR:=/var/lib/letsencrypt}"
78
: "${CERTBOT_DEBUG:=false}"
@@ -10,37 +11,75 @@
1011
: "${CERTBOT_RSA_KEY_SIZE:=2048}"
1112
: "${CERTBOT_STAGING:=false}"
1213
: "${CERTBOT_SUBSET:=true}"
14+
: "${CERTBOT_CERT_PER_HOST:=false}"
15+
: "${CERTBOT_COMBINED_CERT_NAME:=openshift-route-certs}"
1316

14-
if [ -z "$CERTBOT_EMAIL" ]; then
17+
if [ -z "${CERTBOT_EMAIL}" ]; then
1518
echo "Missing 'CERTBOT_EMAIL' environment variable"
1619
exit 1
1720
fi
1821

19-
mkdir -p "$CERTBOT_CONFIG_DIR" "$CERTBOT_WORK_DIR" "$CERTBOT_LOGS_DIR" "$CERTBOT_DEPLOY_DIR"
22+
function deleteAcmeChallengeRoutes() {
23+
echo "Deleting ACME challenge resources ..."
24+
oc delete route,svc,networkpolicy -l app=certbot,well-known=acme-challenge
25+
}
26+
27+
function getCertificate() {
28+
certificateName=${1}
29+
domainList=${2}
30+
31+
CERTBOT_ARGS='--no-random-sleep --no-eff-email'
32+
if [ "${CERTBOT_DRY_RUN}" == "true" ]; then
33+
CERTBOT_ARGS="${CERTBOT_ARGS} --dry-run"
34+
fi
35+
36+
if [ "${CERTBOT_DEBUG}" == "true" ]; then
37+
CERTBOT_ARGS="${CERTBOT_ARGS} --debug"
38+
fi
39+
40+
if [ "${CERTBOT_SUBSET}" == "true" ]; then
41+
CERTBOT_ARGS="${CERTBOT_ARGS} --allow-subset-of-names"
42+
fi
43+
44+
if [ ! -z "$CERTBOT_SERVER" ]; then
45+
CERTBOT_ARGS="${CERTBOT_ARGS} --server ${CERTBOT_SERVER}"
46+
fi
47+
48+
set -x
49+
# If there is no certificate issued request a new one, otherwise request a renewal.
50+
if [ ! -f "${CERTBOT_CONFIG_DIR}/live/${certificateName}/cert.pem" ]; then
51+
certbot --config /tmp/certbot.ini certonly $CERTBOT_ARGS --non-interactive --keep-until-expiring --cert-name ${certificateName} --expand --standalone -d ${domainList}
52+
else
53+
certbot --config /tmp/certbot.ini renew $CERTBOT_ARGS --no-random-sleep-on-renew --cert-name ${certificateName}
54+
fi
55+
set +x
56+
}
57+
58+
mkdir -p "${CERTBOT_CONFIG_DIR}" "${CERTBOT_WORK_DIR}" "${CERTBOT_LOGS_DIR}" "${CERTBOT_DEPLOY_DIR}"
2059

2160
cat > /tmp/certbot.ini <<EOF
22-
rsa-key-size = $CERTBOT_RSA_KEY_SIZE
61+
rsa-key-size = ${CERTBOT_RSA_KEY_SIZE}
2362
authenticator = standalone
2463
http-01-port = 8080
2564
https-port = 4443
2665
preferred-challenges = http
2766
agree-tos = true
28-
email = $CERTBOT_EMAIL
67+
email = ${CERTBOT_EMAIL}
2968
30-
config-dir = $CERTBOT_CONFIG_DIR
31-
work-dir = $CERTBOT_WORK_DIR
32-
logs-dir = $CERTBOT_LOGS_DIR
69+
config-dir = ${CERTBOT_CONFIG_DIR}
70+
work-dir = ${CERTBOT_WORK_DIR}
71+
logs-dir = ${CERTBOT_LOGS_DIR}
3372
EOF
3473

3574
if [ "${CERTBOT_STAGING}" == "true" ]; then
3675
echo "staging = true" >> /tmp/certbot.ini
3776
fi
3877

39-
cat > $CERTBOT_CONFIG_DIR/renewal-hooks/deploy/set-deployed-flag.sh << EOF
78+
cat > ${CERTBOT_DEPLOY_DIR}/set-deployed-flag.sh << EOF
4079
#!/bin/sh
41-
touch $CERTBOT_WORK_DIR/deployed
80+
touch ${CERTBOT_WORK_DIR}/deployed
4281
EOF
43-
chmod +x $CERTBOT_CONFIG_DIR/renewal-hooks/deploy/set-deployed-flag.sh
82+
chmod +x ${CERTBOT_DEPLOY_DIR}/set-deployed-flag.sh
4483

4584
cat > /tmp/certbot-svc.yaml <<'EOF'
4685
apiVersion: v1
@@ -118,19 +157,50 @@ spec:
118157
- Ingress
119158
EOF
120159

121-
# Prepare list of sorted and unique managed domains
122-
oc get route -l certbot-managed=true -o=jsonpath='{range .items[*]}{.spec.host}{"\n"}{end}' | sort -fu > /tmp/certbot-hosts.txt
160+
# Get a mapping of all managed routes and their hosts
161+
routeMap=$(oc get route -l certbot-managed=true -o=jsonpath='{range .items[*]}{.metadata.name}={.spec.host}{"\n"}{end}')
162+
163+
# Declare and populate a hash table to use as a dictionary for mapping the routes to their hosts.
164+
# - The host name will also be used as the certificate name in the case individual certificates are being requested.
165+
declare -A managedRoutes
166+
for item in ${routeMap}; do
167+
# Filter out platform routes
168+
if [[ "${item}" != *apps.silver.devops.gov.bc.ca* ]]; then
169+
# Use the route's name as the key
170+
# and the host name as the value
171+
key=${item%%=*}
172+
value=${item#*=}
173+
managedRoutes[${key}]=${value}
174+
fi
175+
done
176+
177+
# Generate a list of sorted and unique managed domains (hosts), and a list of sorted and unique routes
178+
echo "${managedRoutes[@]}" | tr " " "\n" | sort -fu > /tmp/certbot-hosts.txt
123179
cat /tmp/certbot-hosts.txt | paste -sd "," - > /tmp/certbot-hosts.csv
180+
echo "${!managedRoutes[@]}" | tr " " "\n" | sort -fu > /tmp/certbot-routes.txt
124181

125-
echo 'CERTBOT_DEBUG =' $CERTBOT_DEBUG
182+
echo 'CERTBOT_DEBUG =' ${CERTBOT_DEBUG}
126183
# Dump contents of files to help troubleshoot in case of problems
127184
if [ "${CERTBOT_DEBUG}" == "true" ]; then
185+
echo '*********** full list of detected routes (route=host):'
186+
for item in ${routeMap}; do
187+
echo " ${item}"
188+
done
189+
190+
echo '*********** resulting filtered mapping of routes to hosts (certificate names):'
191+
for route in "${!managedRoutes[@]}"; do
192+
echo " ${route}: ${managedRoutes[${route}]}"
193+
done
194+
128195
echo '*********** contents of /tmp/certbot-hosts.csv:'
129196
cat /tmp/certbot-hosts.csv
130197

131198
echo '*********** contents of /tmp/certbot-hosts.txt:'
132199
cat /tmp/certbot-hosts.txt
133200

201+
echo '*********** contents of /tmp/certbot-routes.txt:'
202+
cat /tmp/certbot-routes.txt
203+
134204
echo '*********** contents of /tmp/certbot-route.yaml:'
135205
cat /tmp/certbot-route.yaml
136206

@@ -144,11 +214,8 @@ if [ "${CERTBOT_DEBUG}" == "true" ]; then
144214
cat /tmp/certbot.ini
145215
fi
146216

147-
# List of Routes
148-
oc get route -l certbot-managed=true -o=jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | sort -fu > /tmp/certbot-routes.txt
149-
150217
# Delete well-known/acme-challenge routes
151-
oc delete route,svc -l app=certbot,well-known=acme-challenge
218+
deleteAcmeChallengeRoutes
152219

153220
# Create certbot network policy
154221
oc create -f /tmp/certbot-np.yaml
@@ -162,79 +229,77 @@ cat /tmp/certbot-hosts.txt | xargs -n 1 -I {} oc process -f /tmp/certbot-route.y
162229
# Sleep for 5sec. There was an issue noticed where the pod wasn't able to get a route and was giving 404 error. Not totally certain if this helps.
163230
sleep 5s
164231

165-
rm -f $CERTBOT_WORK_DIR/deployed
166-
CERTBOT_ARGS='--no-random-sleep --no-eff-email'
167-
if [ "${CERTBOT_DRY_RUN}" == "true" ]; then
168-
CERTBOT_ARGS="${CERTBOT_ARGS} --dry-run"
169-
fi
232+
rm -f ${CERTBOT_WORK_DIR}/deployed
170233

171-
if [ "${CERTBOT_DEBUG}" == "true" ]; then
172-
CERTBOT_ARGS="${CERTBOT_ARGS} --debug"
173-
fi
174-
175-
if [ "${CERTBOT_SUBSET}" == "true" ]; then
176-
CERTBOT_ARGS="${CERTBOT_ARGS} --allow-subset-of-names"
177-
fi
178-
179-
if [ ! -z "$CERTBOT_SERVER" ]; then
180-
CERTBOT_ARGS="${CERTBOT_ARGS} --server ${CERTBOT_SERVER}"
181-
fi
182-
183-
set -x
184-
# if there is no certificate issue, request a new one
185-
if [ ! -f "${CERTBOT_CONFIG_DIR}/live/openshift-route-certs/cert.pem" ]; then
186-
certbot --config /tmp/certbot.ini certonly $CERTBOT_ARGS --non-interactive --keep-until-expiring --cert-name 'openshift-route-certs' --expand --standalone -d "$(</tmp/certbot-hosts.csv)"
234+
# Get certificate(s), either combined or individual
235+
if [ "${CERTBOT_CERT_PER_HOST}" == "true" ]; then
236+
echo "Manage individual certificates for each unique host."
237+
for certbot_host in $(</tmp/certbot-hosts.txt); do
238+
getCertificate "${certbot_host}" "${certbot_host}"
239+
done
187240
else
188-
# if a certificate already exists, request to renew it
189-
certbot --config /tmp/certbot.ini renew $CERTBOT_ARGS --no-random-sleep-on-renew --cert-name 'openshift-route-certs'
190-
fi
191-
set +x
241+
echo "Managing a single certificate covering all managed hosts."
242+
getCertificate "${CERTBOT_COMBINED_CERT_NAME}" "$(</tmp/certbot-hosts.csv)"
192243

193-
if [ "${CERTBOT_DRY_RUN}" == "true" ]; then
194-
exit 0
244+
# Re-Map the managed route dictionary so all routes get patched with the combined certificate.
245+
for route in "${!managedRoutes[@]}"; do
246+
managedRoutes[${route}]="${CERTBOT_COMBINED_CERT_NAME}"
247+
done
195248
fi
196249

197-
if [ -f $CERTBOT_WORK_DIR/deployed ]; then
250+
if [ -f ${CERTBOT_WORK_DIR}/deployed ]; then
198251
echo 'New certificate(s) have been issued'
199252
else
200-
echo 'No certificate(s) have been issued'
253+
echo 'No new certificate(s) have been issued'
201254
fi
202255

203-
# if [ -f $CERTBOT_WORK_DIR/deployed ]; then
204-
echo 'Updating Routes'
205-
CERTIFICATE="$(awk '{printf "%s\\n", $0}' $CERTBOT_CONFIG_DIR/live/openshift-route-certs/cert.pem)"
206-
KEY="$(awk '{printf "%s\\n", $0}' $CERTBOT_CONFIG_DIR/live/openshift-route-certs/privkey.pem)"
207-
CABUNDLE=$(awk '{printf "%s\\n", $0}' $CERTBOT_CONFIG_DIR/live/openshift-route-certs/fullchain.pem)
256+
# Patch Routes
257+
for route in "${!managedRoutes[@]}"; do
258+
certificateName=${managedRoutes[${route}]}
259+
260+
echo "Updating route/${route} with certificate ${certificateName} ..."
261+
CERTIFICATE="$(awk '{printf "%s\\n", $0}' ${CERTBOT_CONFIG_DIR}/live/${certificateName}/cert.pem)"
262+
KEY="$(awk '{printf "%s\\n", $0}' ${CERTBOT_CONFIG_DIR}/live/${certificateName}/privkey.pem)"
263+
CABUNDLE=$(awk '{printf "%s\\n", $0}' ${CERTBOT_CONFIG_DIR}/live/${certificateName}/fullchain.pem)
264+
265+
# If any of the cert components is blank, then don't run the patch command
266+
if [ "${CERTBOT_DRY_RUN}" == "true" ]; then
267+
echo "Dry Run - the certificate for route/${route} was not patched."
268+
elif [ "${CERTIFICATE}" == "" ] || [ "${KEY}" == "" ] || [ "${CABUNDLE}" == "" ]; then
269+
echo "The certificate for route/${route} wasn't created properly so it won't be patched."
270+
else
271+
oc patch "route/${route}" -p '{"spec":{"tls":{"certificate":"'"${CERTIFICATE}"'","key":"'"${KEY}"'","caCertificate":"'"${CABUNDLE}"'"}}}'
272+
fi
273+
done
208274

209-
# If any of the cert components is blank, then don't run the patch command
210-
if [ "${CERTIFICATE}" == "" ] || [ "${KEY}" == "" ] || [ "${CABUNDLE}" == "" ]; then
211-
echo "Certs weren't created properly and no routes were patched."
212-
else
213-
cat /tmp/certbot-routes.txt | xargs -n 1 -I {} oc patch "route/{}" -p '{"spec":{"tls":{"certificate":"'"${CERTIFICATE}"'","key":"'"${KEY}"'","caCertificate":"'"${CABUNDLE}"'"}}}'
214-
fi
215-
216-
# Print the log file if debugging is enabled
217275
if [ "${CERTBOT_DEBUG}" == "true" ]; then
218-
echo '*********** list of all files/folder under /etc/letsencrypt:'
219-
find /etc/letsencrypt
220276

221-
echo '*********** list of all files/folder under /var/log/letsencrypt:'
222-
find /var/log/letsencrypt
277+
echo '*********** final mapping of routes to hosts (certificate names):'
278+
for route in "${!managedRoutes[@]}"; do
279+
echo " ${route}: ${managedRoutes[${route}]}"
280+
done
223281

224-
echo '*********** list of all files/folder under /var/lib/letsencrypt:'
225-
find /var/lib/letsencrypt
282+
echo "*********** list of all files/folder under ${CERTBOT_CONFIG_DIR}:"
283+
find ${CERTBOT_CONFIG_DIR}
226284

227-
echo '*********** contents of /var/log/letsencrypt/letsencrypt.log:'
228-
cat /var/log/letsencrypt/letsencrypt.log
285+
echo "*********** list of all files/folder under ${CERTBOT_LOGS_DIR}:"
286+
find ${CERTBOT_LOGS_DIR}
229287

230-
echo '*********** contents of /etc/letsencrypt/renewal/openshift-route-certs.conf:'
231-
cat /etc/letsencrypt/renewal/openshift-route-certs.conf
288+
echo "*********** list of all files/folder under ${CERTBOT_WORK_DIR}:"
289+
find ${CERTBOT_WORK_DIR}
290+
291+
echo "*********** contents of ${CERTBOT_LOGS_DIR}/letsencrypt.log:"
292+
cat ${CERTBOT_LOGS_DIR}/letsencrypt.log
293+
294+
for confFile in $(ls ${CERTBOT_RENEWAL_DIR} -1); do
295+
echo "*********** contents of ${confFile}:"
296+
cat "${CERTBOT_RENEWAL_DIR}/${confFile}"
297+
done
232298
fi
233299

234300
if [ "${CERTBOT_DELETE_ACME_ROUTES}" == "true" ]; then
235301
# Delete well-known/acme-challenge routes
236-
echo "Deleting ACME service and routes"
237-
oc delete route,svc,networkpolicy -l app=certbot,well-known=acme-challenge
302+
deleteAcmeChallengeRoutes
238303
else
239-
echo "ACME service and routes were not deleted, please clean them up manually."
304+
echo "ACME challenge resources (services, routes, and network policies) were not deleted, please clean them up manually."
240305
fi

openshift/certbot.dc.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ objects:
134134
value: "${CERTBOT_STAGING}"
135135
- name: CERTBOT_SUBSET
136136
value: "${CERTBOT_SUBSET}"
137+
- name: CERTBOT_CERT_PER_HOST
138+
value: "${CERTBOT_CERT_PER_HOST}"
137139
resources:
138140
requests:
139141
cpu: 50m
@@ -188,6 +190,11 @@ parameters:
188190
required: true
189191
value: "true"
190192

193+
- name: CERTBOT_CERT_PER_HOST
194+
description: Manage an individual certificate per unique managed host (domain name), if true, otherwise, manage a single certificate for all managed hosts (domain names)
195+
required: true
196+
value: "false"
197+
191198
- name: CRON_SCHEDULE
192199
description: Cronjob Schedule
193200
required: true

0 commit comments

Comments
 (0)