From 8b9bc903d2a28cd12a9746b3aa4e6846579f3457 Mon Sep 17 00:00:00 2001 From: Adam Gilbert Date: Fri, 4 Jan 2019 12:16:36 -0600 Subject: [PATCH] Add standalone scripts that can be run on hosts joined to the domain but not running freeipa --- foreman-renew.sh | 1 + register-standalone.sh | 128 +++++++++++++++++++++++++++++++++++++ renew-standalone.sh | 141 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100755 register-standalone.sh create mode 100755 renew-standalone.sh diff --git a/foreman-renew.sh b/foreman-renew.sh index 3af5dcb..8920ce5 100755 --- a/foreman-renew.sh +++ b/foreman-renew.sh @@ -9,6 +9,7 @@ kct_dir="/etc/pki/katello-certs-tools" k_dir="/etc/pki/katello" # Get new certificate +# shellcheck disable=2016 certbot certonly --manual \ --preferred-challenges dns \ --manual-public-ip-logging-ok \ diff --git a/register-standalone.sh b/register-standalone.sh new file mode 100755 index 0000000..fe3555c --- /dev/null +++ b/register-standalone.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# Copyright (c) 2017 Antonia Stevens a@antevens.com + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Set strict mode +set -euo pipefail + +# Version +# shellcheck disable=2034 +version='0.0.3' + +# If there is no TTY then it's not interactive +if ! [[ -t 1 ]]; then + interactive=false +fi +# Default is interactive mode unless already set +interactive="${interactive:-true}" + +# Safely loads config file +# First parameter is filename, all consequent parameters are assumed to be +# valid configuration parameters +function load_config() +{ + config_file="${1}" + # Verify config file permissions are correct and warn if they are not + # Dual stat commands to work with both linux and bsd + shift + while read -r line; do + if [[ "${line}" =~ ^[^#]*= ]]; then + setting_name="$(echo "${line}" | awk --field-separator='=' '{print $1}' | sed --expression 's/^[[:space:]]*//' --expression 's/[[:space:]]*$//')" + setting_value="$(echo "${line}" | cut --fields=1 --delimiter='=' --complement | sed --expression 's/^[[:space:]]*//' --expression 's/[[:space:]]*$//')" + + if echo "${@}" | grep -q "${setting_name}" ; then + export "${setting_name}"="${setting_value}" + echo "Loaded config parameter ${setting_name} with value of '${setting_value}'" + fi + fi + done < "${config_file}"; +} + +message=" +This script will modify your FreeIPA setup so that this server can automatically +apply for LetsEncrypt SSL/TLS certificates for all hostnames/principals associted. + +This script needs the host to be already registered in FreeIPA, the IPA client +is installed and that the user running this script is in the IPA admins group. + +The following steps will be taken: + +1. Installing CertBot (Let's Encrypt client) +2. Create a DNS Administrator role in FreeIPA, members of which can edit DNS Records +3. Create a new service, allow it to manage DNS entries +4. Allow members of the admin group to create and retrieve keytabs for the service +5. Create bogus TXT initialization records for the host. +" + +if ${interactive} ; then + while ! [[ "${REPLY:-}" =~ ^[NnYy]$ ]]; do + echo "${message}" + read -rp "Please confirm you want to continue and modify your system/setup (y/n):" -n 1 + echo + + # Get a fresh kerberos ticket if needed + klist || ( [ "${EUID:-$(id -u)}" -eq 0 ] && kinit "${SUDO_USER:-${USER}}" ) || kinit + done +else + REPLY="y" +fi + +if [[ ${REPLY} =~ ^[Yy]$ ]]; then + if [ ! $(command -v certbot) ]; then + echo 'Installing certbot.' + sudo yum -y install certbot || (sudo apt-get update && sudo apt-get -y install certbot) + fi + + if [ ! $(command -v ipa) ]; then + echo 'Installing freeipa utilities.' + sudo yum -y install ipa-client || (sudo apt-get update && sudo apt-get -y install freeipa-admintools) + fi + + load_config '/etc/krb5.conf' default_realm + host="$(hostname)" + group='admins' + # shellcheck disable=2154 + principals="$(ipa host-show "${host}" --raw | grep krbprincipalname | grep 'host/' | sed 's.krbprincipalname: host/..' | sed s/"@${default_realm}"//)" + + ipa service-add "lets-encrypt/${host}@${default_realm}" + ipa role-add "DNS Administrator" + ipa role-add-privilege "DNS Administrator" --privileges="DNS Administrators" + ipa role-add-member "DNS Administrator" --services="lets-encrypt/${host}@${default_realm}" + ipa service-allow-create-keytab "lets-encrypt/${host}@${default_realm}" --groups=${group} + ipa service-allow-retrieve-keytab "lets-encrypt/${host}@${default_realm}" --groups=${group} + ipa-getkeytab -p "lets-encrypt/${host}" -k /etc/lets-encrypt.keytab #add -r to renew + + echo 'FreeIPA service and keytab initialization complete, proceeding with DNS initialization.' + for principal in ${principals} ; do + echo "Adding placeholder ACME challenge TXT for ${principal}" + ipa dnsrecord-add "${principal#[a-zA-Z0-9,\-\_]*\.}." "_acme-challenge.${principal}." --txt-rec='INITIALIZED' + done + + echo 'DNS initialization complete, running letsencrypt process to obtain fresh certificates.' + # Apply for the initial certificate if script is available + renew_script_path="$(dirname "${0}")/renew-standalone.sh" + if [ -f "${renew_script_path}" ] ; then + sudo bash -c "${renew_script_path}" + fi +else + echo "Let's Encrypt registration cancelled by user" + exit 1 +fi diff --git a/renew-standalone.sh b/renew-standalone.sh new file mode 100755 index 0000000..a8feb04 --- /dev/null +++ b/renew-standalone.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Copyright (c) 2017 Antonia Stevens a@antevens.com + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Set strict mode +set -euo pipefail + +# Version +# shellcheck disable=2034 +version='0.0.3' + +# Exit if not being run as root +if [ "${EUID:-$(id -u)}" -ne "0" ] ; then + echo "This script needs superuser privileges, suggest running it as root" + exit 1 +fi + +# Start Unix time +start_time_epoch="$(date +%s)" + +# If there is no TTY then it's not interactive +if ! [[ -t 1 ]]; then + interactive=false +fi +# Default is interactive mode unless already set +interactive="${interactive:-true}" + + +# Safely loads config file +# First parameter is filename, all consequent parameters are assumed to be +# valid configuration parameters +function load_config() +{ + config_file="${1}" + # Verify config file permissions are correct and warn if they are not + # Dual stat commands to work with both linux and bsd + shift + while read -r line; do + if [[ "${line}" =~ ^[^#]*= ]]; then + setting_name="$(echo "${line}" | awk --field-separator='=' '{print $1}' | sed --expression 's/^[[:space:]]*//' --expression 's/[[:space:]]*$//')" + setting_value="$(echo "${line}" | cut --fields=1 --delimiter='=' --complement | sed --expression 's/^[[:space:]]*//' --expression 's/[[:space:]]*$//')" + + if echo "${@}" | grep -q "${setting_name}" ; then + export "${setting_name}"="${setting_value}" + echo "Loaded config parameter ${setting_name} with value of '${setting_value}'" + fi + fi + done < "${config_file}"; +} + +# This script will automatically fetch/renew your LetsEncrypt certificate for all +# defined principals. Before running this script you should run the acompanying +# register script. This script should be scheduled to run from crontab or similar +# as a superuser (root). +# The email address will always default to the hostmaster in the SOA record +# for the first/shortest principal in IPA, this can be overwritten using the +# email environment variable, for example: +# email="admin@example.com" ./renew.sh +load_config '/etc/krb5.conf' default_realm +host="$(hostname)" +# Get kerberos ticket to modify DNS entries +kinit -k -t /etc/lets-encrypt.keytab "lets-encrypt/${host}" +# shellcheck disable=2154 +principals="$(ipa host-show "${host}" --raw | grep krbprincipalname | grep 'host/' | sed 's.krbprincipalname: host/..' | sed s/"@${default_realm}"//)" +dns_domain_name="${host#[a-zA-Z0-9,\-\_]*\.}" +soa_record="$(dig SOA "${dns_domain_name}" + short | grep ^"${dns_domain_name}". | grep 'SOA' | awk '{print $6}')" +hostmaster="${soa_record/\./@}" +email="${email:-${hostmaster%\.}}" +letsencrypt_live_dir="/etc/letsencrypt/live" +letsencrypt_pem_dir="$(find -L ${letsencrypt_live_dir} -newermt "@${start_time_epoch}" -type f -name 'privkey.pem' -exec dirname {} \;)" + +# Configure the manual auth hook +# shellcheck disable=2016 +default_auth_hook='ipa dnsrecord-mod ${CERTBOT_DOMAIN#*.}. _acme-challenge.${CERTBOT_DOMAIN}. --txt-rec=${CERTBOT_VALIDATION}' + +# Configure alternative nsupdate hook +nsupdate_auth_server="${NSUPDATE_AUTH_SERVER:-$(nslookup -type=soa "${dns_domain_name}" | grep 'origin =' | sed -e 's/[[:space:]]*origin = //')}" +#shellcheck disable=2016 +nsupdate_commands=( + "server ${nsupdate_auth_server}" + 'update delete _acme-challenge.${CERTBOT_DOMAIN} TXT' + 'update add _acme-challenge.${CERTBOT_DOMAIN} 320 IN TXT "${CERTBOT_VALIDATION}' + 'send' +) +nsupdate_key_name="${NSUPDATE_KEY_NAME:-}" +nsupdate_key_secret="${NSUPDATE_KEY_SECRET:-}" +nsupdate_key_file="${NSUPDATE_KEY_FILE:-}" +nsupdate_auth_hook='printf "%s\n" '"${nsupdate_commands[*]} | nsupdate -v" +# Prefer key file but also accept key_name/secret combo +if [ -n "${nsupdate_key_file}" ] ; then + if [ -e "${nsupdate_key_file}" ] ; then + default_auth_hook="${nsupdate_auth_hook} -k ${nsupdate_key_file}" + else + echo "Specified nsupdate key file ${nsupdate_key_file} does not exist, exiting!" + exit 1 + fi +elif [ -n "${nsupdate_key_name}" ] && [ -n "${nsupdate_key_secret}" ] ; then + default_auth_hook="${nsupdate_auth_hook} -y ${nsupdate_key_name}:${nsupdate_key_secret}" +fi + +# Set the auth hook +auth_hook="${AUTH_HOOK:-${default_auth_hook}}" + +domains=($(echo ${principals} | tr " " "\n")) +for domain in "${domains[@]}" ; do + domain_args+=("-d ${domain}") +done + +# Apply for a new cert using CertBot with DNS verification +certbot certonly --manual \ + --preferred-challenges dns \ + --manual-public-ip-logging-ok \ + --manual-auth-hook "${auth_hook}" \ + "${domain_args[@]}" \ + --agree-tos \ + --email "${email}" \ + --expand \ + -n + +# If the certificate has been updated since start of this script +if [ -n "${letsencrypt_pem_dir}" ] ; then + echo 'Certificate has been updated, you will now have to restart your web server.' +fi