dumpcerts_traefik 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env bash
  2. # Copyright (c) 2017 Brian 'redbeard' Harrington <redbeard@dead-city.org>
  3. #
  4. # dumpcerts.sh - A simple utility to explode a Traefik acme.json file into a
  5. # directory of certificates and a private key
  6. #
  7. # Usage - dumpcerts.sh /etc/traefik/acme.json /etc/ssl/
  8. #
  9. # Dependencies -
  10. # util-linux
  11. # openssl
  12. # jq
  13. # The MIT License (MIT)
  14. #
  15. # Permission is hereby granted, free of charge, to any person obtaining a copy
  16. # of this software and associated documentation files (the "Software"), to deal
  17. # in the Software without restriction, including without limitation the rights
  18. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  19. # copies of the Software, and to permit persons to whom the Software is
  20. # furnished to do so, subject to the following conditions:
  21. #
  22. # The above copyright notice and this permission notice shall be included in
  23. # all copies or substantial portions of the Software.
  24. #
  25. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  26. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  27. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  28. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  29. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  30. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  31. # THE SOFTWARE.
  32. # Exit codes:
  33. # 1 - A component is missing or could not be read
  34. # 2 - There was a problem reading acme.json
  35. # 4 - The destination certificate directory does not exist
  36. # 8 - Missing private key
  37. set -o errexit
  38. set -o pipefail
  39. set -o nounset
  40. USAGE="$(basename "$0") <path to acme> <destination cert directory>"
  41. # Platform variations
  42. case "$(uname)" in
  43. 'Linux')
  44. # On Linux, -d should always work. --decode does not work with Alpine's busybox-binary
  45. CMD_DECODE_BASE64="base64 -d"
  46. ;;
  47. *)
  48. # Max OS-X supports --decode and -D, but --decode may be supported by other platforms as well.
  49. CMD_DECODE_BASE64="base64 --decode"
  50. ;;
  51. esac
  52. # Allow us to exit on a missing jq binary
  53. exit_jq() {
  54. echo "
  55. You must have the binary 'jq' to use this.
  56. jq is available at: https://stedolan.github.io/jq/download/
  57. ${USAGE}" >&2
  58. exit 1
  59. }
  60. bad_acme() {
  61. echo "
  62. There was a problem parsing your acme.json file.
  63. ${USAGE}" >&2
  64. exit 2
  65. }
  66. if [ $# -ne 2 ]; then
  67. echo "
  68. Insufficient number of parameters.
  69. ${USAGE}" >&2
  70. exit 1
  71. fi
  72. readonly acmefile="${1}"
  73. readonly certdir="${2%/}"
  74. if [ ! -r "${acmefile}" ]; then
  75. echo "
  76. There was a problem reading from '${acmefile}'
  77. We need to read this file to explode the JSON bundle... exiting.
  78. ${USAGE}" >&2
  79. exit 2
  80. fi
  81. if [ ! -d "${certdir}" ]; then
  82. echo "
  83. Path ${certdir} does not seem to be a directory
  84. We need a directory in which to explode the JSON bundle... exiting.
  85. ${USAGE}" >&2
  86. exit 4
  87. fi
  88. jq=$(command -v jq) || exit_jq
  89. priv=$(${jq} -e -r '.[].Account.PrivateKey' "${acmefile}") || bad_acme
  90. if [ ! -n "${priv}" ]; then
  91. echo "
  92. There didn't seem to be a private key in ${acmefile}.
  93. Please ensure that there is a key in this file and try again." >&2
  94. exit 8
  95. fi
  96. # If they do not exist, create the needed subdirectories for our assets
  97. # and place each in a variable for later use, normalizing the path
  98. mkdir -p "${certdir}"/{certs,private}
  99. pdir="${certdir}/private/"
  100. cdir="${certdir}/certs/"
  101. # Save the existing umask, change the default mode to 600, then
  102. # after writing the private key switch it back to the default
  103. oldumask=$(umask)
  104. umask 177
  105. trap 'umask ${oldumask}' EXIT
  106. # traefik stores the private key in stripped base64 format but the certificates
  107. # bundled as a base64 object without stripping headers. This normalizes the
  108. # headers and formatting.
  109. #
  110. # In testing this out it was a balance between the following mechanisms:
  111. # gawk:
  112. # echo ${priv} | awk 'BEGIN {print "-----BEGIN RSA PRIVATE KEY-----"}
  113. # {gsub(/.{64}/,"&\n")}1
  114. # END {print "-----END RSA PRIVATE KEY-----"}' > "${pdir}/letsencrypt.key"
  115. #
  116. # openssl:
  117. # echo -e "-----BEGIN RSA PRIVATE KEY-----\n${priv}\n-----END RSA PRIVATE KEY-----" \
  118. # | openssl rsa -inform pem -out "${pdir}/letsencrypt.key"
  119. #
  120. # and sed:
  121. # echo "-----BEGIN RSA PRIVATE KEY-----" > "${pdir}/letsencrypt.key"
  122. # echo ${priv} | sed -E 's/(.{64})/\1\n/g' >> "${pdir}/letsencrypt.key"
  123. # sed -i '$ d' "${pdir}/letsencrypt.key"
  124. # echo "-----END RSA PRIVATE KEY-----" >> "${pdir}/letsencrypt.key"
  125. # openssl rsa -noout -in "${pdir}/letsencrypt.key" -check # To check if the key is valid
  126. # In the end, openssl was chosen because most users will need this script
  127. # *because* of openssl combined with the fact that it will refuse to write the
  128. # key if it does not parse out correctly. The other mechanisms were left as
  129. # comments so that the user can choose the mechanism most appropriate to them.
  130. echo -e "-----BEGIN RSA PRIVATE KEY-----\n${priv}\n-----END RSA PRIVATE KEY-----" \
  131. | openssl rsa -inform pem -out "${pdir}/letsencrypt.key" >/dev/null
  132. # Process the certificates for each of the domains in acme.json
  133. for domain in $(jq -r '.[].Certificates[].domain.main' ${acmefile}); do
  134. # Traefik stores a cert bundle for each domain. Within this cert
  135. # bundle there is both proper the certificate and the Let's Encrypt CA
  136. echo "Extracting cert bundle for ${domain}"
  137. cert=$(jq -e -r --arg domain "$domain" '.[].Certificates[] |
  138. select (.domain.main == $domain )| .certificate' ${acmefile}) || bad_acme
  139. echo "${cert}" | ${CMD_DECODE_BASE64} > "${cdir}/${domain}.crt"
  140. echo "Extracting private key for ${domain}"
  141. key=$(jq -e -r --arg domain "$domain" '.[].Certificates[] |
  142. select (.domain.main == $domain )| .key' ${acmefile}) || bad_acme
  143. echo "${key}" | ${CMD_DECODE_BASE64} > "${pdir}/${domain}.key"
  144. done