#! /bin/bash
#
set -e
set -u

export PATH="/opt/puppet/bin:$PATH"

# The intermediate certificate name is a required argument
if [[ -z "${1:-}" ]]; then
  echo "You must specify an intermediate certname for the puppet master as argument 1. Your site's DNS should be configured to resolve this new name to the puppet master." >&2
  echo "e.g. $(basename ${0}) puppetmaster.new" >&2
  exit 1
else
  new_master_cert="${1}"
  shift
fi

vardir="$(puppet master --configprint vardir)"

# Figure out if we've been patched or not.
if puppet master --configprint dns_alt_names &>/dev/null; then
  HAVE_PATCHED='true'
  alt_names_option="dns_alt_names"
  alt_names_separator=","
  # If the user has upgraded, but not reconfigured, then this might actually be
  # empty and certdnsnames is still in use.
  alt_names_value="$(puppet master --configprint dns_alt_names)"
  if [[ -z "${alt_names_value}" ]]; then
    alt_names_value="$(puppet master --configprint certdnsnames 2>/dev/null | sed 's/:/,/g')"
  fi
else
  HAVE_PATCHED='false'
  alt_names_option="certdnsnames"
  alt_names_separator=":"
  alt_names_value="$(puppet master --configprint certdnsnames)"
fi

# Write the new DNS name to ${vardir}/cve20113872/dns_name
if [[ ! -d "${vardir}/cve20113872" ]]; then
  mkdir -p "${vardir}/cve20113872"
  # Avoid umask issues
  chmod 755 "${vardir}/cve20113872"
fi
echo "${new_master_cert}" > "${vardir}/cve20113872/dns_name"
chmod 644 "${vardir}/cve20113872/dns_name"
echo "${new_master_cert}${alt_names_separator}${alt_names_value}" > "${vardir}/cve20113872/alt_names"
chmod 644 "${vardir}/cve20113872/alt_names"

timestamp="$(ruby -e 'puts Time.now.to_i')"

echo -n "Stopping Puppet Master..." >&2
puppet resource service pe-httpd ensure=stopped hasstatus=true &> /dev/null
echo "done." >&2

# As LAK points out, configuring Puppet to communicate with a master
# using a name that is not in CN or CERTDNSNAMES will secure the
# entire system again.
# We need a new certificate to do this...
old_master_cert="$(puppet master --configprint certname)"

apachevhost="/etc/puppetlabs/httpd/conf.d/puppetmaster.conf"

confdir="$(puppet master --configprint confdir)"
ssldir="$(puppet master --configprint ssldir)"
manifest="$(puppet master --configprint manifest)"
puppetconf="$(puppet master --configprint config)"
autosign="${confdir}/autosign.conf"
hostcrl="$(puppet master --configprint hostcrl)"

# The new CA CN _must_ be different than the old CA CN
old_ca_cn="$(puppet master --configprint ca_name)"

backup="/etc/puppetlabs/cve20113872.orig.tar.gz"

# Before modifying anything, make a backup
if [[ -f "${backup}" ]]; then
  echo "A backup already exists!  You should restore from this backup" >&2
  echo "using the pe_restore_original_state helper script, then remove" >&2
  echo "the backup at ${backup} before running this script again." >&2
  exit 1
else
  backup_list=$(mktemp -t cve20113872.backup.lst.XXXXXXXXXX)
  echo "${apachevhost}" >> "${backup_list}"
  echo "${ssldir}"      >> "${backup_list}"
  echo "${manifest}"    >> "${backup_list}"
  echo "${puppetconf}"  >> "${backup_list}"
  # PE Masters only run on Linux, so I'm going to assume GNU tar
  tar --files-from "${backup_list}" -czf "${backup}" 2>/dev/null >/dev/null
  echo "Backup written to: ${backup}" >&2
fi

# If this starts at 0, incrementing it will exit nonzero
# See: https://gist.github.com/1310371
# and https://github.com/puppetlabs/puppetlabs-cve20113872/issues/69
idx=1

# Make sure certdnsnames are off.  This will prevent the master from issuing
# additional agent certificates that may be used to impersonate the master.
echo -n "Making sure certdnsnames are turned off (${puppetconf}) ..." >&2
ruby -p -l -i.backup.${timestamp}.${idx} -e \
  'gsub(/^(\s*)(certdnsnames\b.*$)/) { "#{$1}# Disabled to mitigate CVE-2011-3872\n#{$1}# #{$2}" }' \
  "${puppetconf}"
((idx++))
echo "done." >&2

# Generate the new SSL certificate using the old CA
# Note, we actually replace the existing SSL certificate and effectively add another Subject Alt Name to the list.
# There are a bunch of edge cases where the agent on the master may or may not be using the same certificate as
# the master server itself.  To avoid these issues, I want to keep the master cert "as is" and just add a SAN to it.
# This strategy also avoids having to reconfigure apache and puppet.conf.  They can remain as is.
echo -n "Re-issuing a new SSL certificate for ${old_master_cert} with intermediate DNS name ${new_master_cert} ..." >&2
puppet cert --clean "${old_master_cert}" 2>&1 >/dev/null
puppet cert --generate "--${alt_names_option}" "${new_master_cert}${alt_names_separator}${alt_names_value}" "${old_master_cert}" 2>&1 >/dev/null
echo "done." >&2


# At this point, existing agents should be able to communicate with the master.  New certificates
# will be signed by the new CA.
echo -n "Starting Puppet Master..." >&2
puppet resource service pe-httpd ensure=running hasstatus=true 2>&1 >/dev/null
echo "done." >&2

# Run Puppet Agent so that MCollective Self-Heals itself.
echo -n "Running Puppet Agent..." >&2
# Without the ||true we never get beyond this line because of set -e
puppet agent --test 2>/dev/null >/dev/null || true
echo "done." >&2

cat <<EOMESSAGE
Your master has been reconfigured with an intermediate DNS alt name
(${new_master_cert}) to mitigate CVE-2011-3872 Your agents will not be secured
until they are configured to contact the master at this new name.

If your site's DNS has been configured to resolve this name, your agents will be
able to run with the following manual command:

    puppet agent --test --server ${new_master_cert}

You may wish to test at least one agent this way to ensure that the next step
will work.

Please continue to step 2 to automatically reconfigure all of your puppet
agent nodes to use the secure intermediate DNS name.

EOMESSAGE
