1#!/usr/bin/env sh
2
3# Script for acme.sh to deploy certificates to haproxy
4#
5# The following variables can be exported:
6#
7# export DEPLOY_HAPROXY_PEM_NAME="${domain}.pem"
8#
9# Defines the name of the PEM file.
10# Defaults to "<domain>.pem"
11#
12# export DEPLOY_HAPROXY_PEM_PATH="/etc/haproxy"
13#
14# Defines location of PEM file for HAProxy.
15# Defaults to /etc/haproxy
16#
17# export DEPLOY_HAPROXY_RELOAD="systemctl reload haproxy"
18#
19# OPTIONAL: Reload command used post deploy
20# This defaults to be a no-op (ie "true").
21# It is strongly recommended to set this something that makes sense
22# for your distro.
23#
24# export DEPLOY_HAPROXY_ISSUER="no"
25#
26# OPTIONAL: Places CA file as "${DEPLOY_HAPROXY_PEM}.issuer"
27# Note: Required for OCSP stapling to work
28#
29# export DEPLOY_HAPROXY_BUNDLE="no"
30#
31# OPTIONAL: Deploy this certificate as part of a multi-cert bundle
32# This adds a suffix to the certificate based on the certificate type
33# eg RSA certificates will have .rsa as a suffix to the file name
34# HAProxy will load all certificates and provide one or the other
35# depending on client capabilities
36# Note: This functionality requires HAProxy was compiled against
37# a version of OpenSSL that supports this.
38#
39
40########  Public functions #####################
41
42#domain keyfile certfile cafile fullchain
43haproxy_deploy() {
44  _cdomain="$1"
45  _ckey="$2"
46  _ccert="$3"
47  _cca="$4"
48  _cfullchain="$5"
49
50  # Some defaults
51  DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy"
52  DEPLOY_HAPROXY_PEM_NAME_DEFAULT="${_cdomain}.pem"
53  DEPLOY_HAPROXY_BUNDLE_DEFAULT="no"
54  DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
55  DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
56
57  _debug _cdomain "${_cdomain}"
58  _debug _ckey "${_ckey}"
59  _debug _ccert "${_ccert}"
60  _debug _cca "${_cca}"
61  _debug _cfullchain "${_cfullchain}"
62
63  # PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
64  _getdeployconf DEPLOY_HAPROXY_PEM_PATH
65  _debug2 DEPLOY_HAPROXY_PEM_PATH "${DEPLOY_HAPROXY_PEM_PATH}"
66  if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then
67    Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
68    _savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}"
69  elif [ -z "${Le_Deploy_haproxy_pem_path}" ]; then
70    Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
71  fi
72
73  # Ensure PEM_PATH exists
74  if [ -d "${Le_Deploy_haproxy_pem_path}" ]; then
75    _debug "PEM_PATH ${Le_Deploy_haproxy_pem_path} exists"
76  else
77    _err "PEM_PATH ${Le_Deploy_haproxy_pem_path} does not exist"
78    return 1
79  fi
80
81  # PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
82  _getdeployconf DEPLOY_HAPROXY_PEM_NAME
83  _debug2 DEPLOY_HAPROXY_PEM_NAME "${DEPLOY_HAPROXY_PEM_NAME}"
84  if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then
85    Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}"
86    _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
87  elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then
88    Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
89  fi
90
91  # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
92  _getdeployconf DEPLOY_HAPROXY_BUNDLE
93  _debug2 DEPLOY_HAPROXY_BUNDLE "${DEPLOY_HAPROXY_BUNDLE}"
94  if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then
95    Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}"
96    _savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}"
97  elif [ -z "${Le_Deploy_haproxy_bundle}" ]; then
98    Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
99  fi
100
101  # ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
102  _getdeployconf DEPLOY_HAPROXY_ISSUER
103  _debug2 DEPLOY_HAPROXY_ISSUER "${DEPLOY_HAPROXY_ISSUER}"
104  if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then
105    Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}"
106    _savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}"
107  elif [ -z "${Le_Deploy_haproxy_issuer}" ]; then
108    Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
109  fi
110
111  # RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
112  _getdeployconf DEPLOY_HAPROXY_RELOAD
113  _debug2 DEPLOY_HAPROXY_RELOAD "${DEPLOY_HAPROXY_RELOAD}"
114  if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then
115    Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}"
116    _savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}"
117  elif [ -z "${Le_Deploy_haproxy_reload}" ]; then
118    Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
119  fi
120
121  # Set the suffix depending if we are creating a bundle or not
122  if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
123    _info "Bundle creation requested"
124    # Initialise $Le_Keylength if its not already set
125    if [ -z "${Le_Keylength}" ]; then
126      Le_Keylength=""
127    fi
128    if _isEccKey "${Le_Keylength}"; then
129      _info "ECC key type detected"
130      _suffix=".ecdsa"
131    else
132      _info "RSA key type detected"
133      _suffix=".rsa"
134    fi
135  else
136    _suffix=""
137  fi
138  _debug _suffix "${_suffix}"
139
140  # Set variables for later
141  _pem="${Le_Deploy_haproxy_pem_path}/${Le_Deploy_haproxy_pem_name}${_suffix}"
142  _issuer="${_pem}.issuer"
143  _ocsp="${_pem}.ocsp"
144  _reload="${Le_Deploy_haproxy_reload}"
145
146  _info "Deploying PEM file"
147  # Create a temporary PEM file
148  _temppem="$(_mktemp)"
149  _debug _temppem "${_temppem}"
150  cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}"
151  _ret="$?"
152
153  # Check that we could create the temporary file
154  if [ "${_ret}" != "0" ]; then
155    _err "Error code ${_ret} returned during PEM file creation"
156    [ -f "${_temppem}" ] && rm -f "${_temppem}"
157    return ${_ret}
158  fi
159
160  # Move PEM file into place
161  _info "Moving new certificate into place"
162  _debug _pem "${_pem}"
163  cat "${_temppem}" >"${_pem}"
164  _ret=$?
165
166  # Clean up temp file
167  [ -f "${_temppem}" ] && rm -f "${_temppem}"
168
169  # Deal with any failure of moving PEM file into place
170  if [ "${_ret}" != "0" ]; then
171    _err "Error code ${_ret} returned while moving new certificate into place"
172    return ${_ret}
173  fi
174
175  # Update .issuer file if requested
176  if [ "${Le_Deploy_haproxy_issuer}" = "yes" ]; then
177    _info "Updating .issuer file"
178    _debug _issuer "${_issuer}"
179    cat "${_cca}" >"${_issuer}"
180    _ret="$?"
181
182    if [ "${_ret}" != "0" ]; then
183      _err "Error code ${_ret} returned while copying issuer/CA certificate into place"
184      return ${_ret}
185    fi
186  else
187    [ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists"
188  fi
189
190  # Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option
191  if [ -z "${Le_OCSP_Staple}" ]; then
192    Le_OCSP_Staple="0"
193  fi
194  if [ "${Le_OCSP_Staple}" = "1" ]; then
195    _info "Updating OCSP stapling info"
196    _debug _ocsp "${_ocsp}"
197    _info "Extracting OCSP URL"
198    _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
199    _debug _ocsp_url "${_ocsp_url}"
200
201    # Only process OCSP if URL was present
202    if [ "${_ocsp_url}" != "" ]; then
203      # Extract the hostname from the OCSP URL
204      _info "Extracting OCSP URL"
205      _ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3)
206      _debug _ocsp_host "${_ocsp_host}"
207
208      # Only process the certificate if we have a .issuer file
209      if [ -r "${_issuer}" ]; then
210        # Check if issuer cert is also a root CA cert
211        _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
212        _debug _subjectdn "${_subjectdn}"
213        _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
214        _debug _issuerdn "${_issuerdn}"
215        _info "Requesting OCSP response"
216        # If the issuer is a CA cert then our command line has "-CAfile" added
217        if [ "${_subjectdn}" = "${_issuerdn}" ]; then
218          _cafile_argument="-CAfile \"${_issuer}\""
219        else
220          _cafile_argument=""
221        fi
222        _debug _cafile_argument "${_cafile_argument}"
223        # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
224        _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
225        _debug _openssl_version "${_openssl_version}"
226        _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
227        _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
228        if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
229          _header_sep="="
230        else
231          _header_sep=" "
232        fi
233        # Request the OCSP response from the issuer and store it
234        _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
235          -issuer \"${_issuer}\" \
236          -cert \"${_pem}\" \
237          -url \"${_ocsp_url}\" \
238          -header Host${_header_sep}\"${_ocsp_host}\" \
239          -respout \"${_ocsp}\" \
240          -verify_other \"${_issuer}\" \
241          ${_cafile_argument} \
242          | grep -q \"${_pem}: good\""
243        _debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}"
244        eval "${_openssl_ocsp_cmd}"
245        _ret=$?
246      else
247        # Non fatal: No issuer file was present so no OCSP stapling file created
248        _err "OCSP stapling in use but no .issuer file was present"
249      fi
250    else
251      # Non fatal: No OCSP url was found int the certificate
252      _err "OCSP update requested but no OCSP URL was found in certificate"
253    fi
254
255    # Non fatal: Check return code of openssl command
256    if [ "${_ret}" != "0" ]; then
257      _err "Updating OCSP stapling failed with return code ${_ret}"
258    fi
259  else
260    # An OCSP file was already present but certificate did not have OCSP extension
261    if [ -f "${_ocsp}" ]; then
262      _err "OCSP was not requested but .ocsp file exists."
263      # Could remove the file at this step, although HAProxy just ignores it in this case
264      # rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file"
265    fi
266  fi
267
268  # Reload HAProxy
269  _debug _reload "${_reload}"
270  eval "${_reload}"
271  _ret=$?
272  if [ "${_ret}" != "0" ]; then
273    _err "Error code ${_ret} during reload"
274    return ${_ret}
275  else
276    _info "Reload successful"
277  fi
278
279  return 0
280}
281