Automatisch updaten van DANE TLSA records

(English below) Als webhoster is mijn streven om uitsluitend internet.nl compliant webhosting aan te bieden. Dat betekent onder andere dat ik websites voortaan uitsluitend over het veilige HTTPS wil aanbieden. Door de komst van Let’s Encrypt is dat relatief gemakkelijk en ook nog eens gratis, maar toch bleef ik het gebruik ervan steeds uitstellen. Ondanks de hoge vernieuwingsfrequentie van Let’s Encrypt certificaten (90 dagen), is het automatisch vernieuwen van de certificaten volledig te automatiseren met Certbot snel voor elkaar te krijgen. Echter, voor het vernieuwen van de certificaat vingerafdruk (SHA256 hash) in het TLSA record, vond ik geen kant-en-klare oplossing die paste bij mijn implementatie en wensen.

Voor het hergebruiken van hetzelfde sleutelpaar voel ik niet zoveel, en het auto-update-DANE script van Jurrian van Iersel vereist de nodige aanpassingen om het te krijgen zoals ik wil. Dan zelf maar even een uurtje scripten dacht ik. Dat uurtje werden er wat meer, maar na verschillende testronden werkt het precies zoals ik wil. Hieronder de code van het script voor eenieder die erin geïnteresseerd is. Ik heb veel comments in de code toegevoegd om uit te leggen wat er gebeurt. Het script is gericht op DANE-for-web, maar kan met een beetje creativiteit worden omgeschreven naar DANE-for-email. Met iets meer creativiteit is dit script ook te gebruiken voor het automatisch roteren van DKIM sleutels; wellicht iets voor een volgende blogpost. Anyway; happy scripting!

English translation

As a webhoster I would like to solely offer webhosting services which are internet.nl compliant. Amoung other things, this means that all websites will only be offered over HTTPS. Let’s Encrypt makes this relatively easy and while it is offered for free, I still kept postponing it’s usage. Despite the high renewal frequency of Let’s Encrypt certificates (90 days), the renewal proces can be automated using Certbot. However, for the renewal of certificate fingerprints (SHA256 hash) in TLSA records, I did not find an out-of-the-box solution that suited my implementation and matched my criteria.

I did not want to reuse the same keypair, and Jurrian van Iersel’s auto-update-DANE script would require some adjustments to get it the way I want it to be. An hour’s work when I script it myself I thought. Eventually it took me several hours, but after some testing it worked exactly as I wanted. The script’s code is shared below for whoever is interested in using it. I’ve added a lot of comments in the code in order to explain what’s happening. The script is made for DANE-for-web, but with some creativity it can quite easily be rewritten to support DANE-for-email. With a little more creativity this script can also be used to automate the rotation of DKIM keys; maybe something for a next blog post. Anyway; happy scripting!

Bash script

Update 17-6-2018: released new version of this script.

#!/bin/bash

## Script for automatically changing DANE TLSA hashes, after auto renewing Letsencrypt certificates 
## Version: 20180617 

#### LICENSE INFORMATION
## Copyright 2018 Dennis Baaten (Baaten ICT Security) 
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
##    http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.

#### ASSUMPTIONS WHILE MAKING THIS SCRIPT
## This script should be run every hour using cron 
## Letsencrypt certificates are added like this: certbot certonly --webroot -w /var/www/www.example.com/ -d www.example.com -d example.com
## There is a DANE record present in the DNS zone (from the initial setup).
## Using Bind as a DNS server
## Using OpenDNSSEC for signing zonefiles
## Logrotate causes apache to auto reload every 24 hours (cron.daily). Therefore I'm using custom symlinks in the letsencrypt 'live' directory
## Root domain and www subdomain have the same certificate
## Zonefile does only contain DANE hash for root domain and www subdomain
## Default TLSA 1 0 1 is being used for all domains; being respectively the certificate usage field, the selector field, matching type field.

#### VERSION HISTORY
##
## Version 20180615
## - first release.
##
## Version 20180617
## - fixed a bug where not all domains were processed.
## - only replace DANE hashes with certificate usage 1. As a result trusted CA DANE records (usage type 3) are not affected.
## - added print status to stdout.
## 

#### SCRIPT START

## Create file with the bare domainnames
certbot certificates | grep "Domains:" | sed -e 's/^[ \t]*//' | cut -f1 --complement -d " " | tr " " "\n" > /etc/letsencrypt/letsencryptdomains
sed -i "/www/d" /etc/letsencrypt/letsencryptdomains

for domainname in $(cat /etc/letsencrypt/letsencryptdomains)
do
  #### First let's set some variables

  ## The current unix time (number of seconds since 1-1-1970)
        current_epoch="$(exec date '+%s')"
  ## The unix time of the last known modify date of the letsencrypt certificate file
  cert_file_renewal_date="$(exec stat $(readlink -f /etc/letsencrypt/live/www.$domainname/cert.pem) | grep Modify | cut -f1 --complement -d " " | sed 's/^[ \t]*//;s/[ \t]*$//')"
  cert_file_renewal_epoch="$(date --date="$cert_file_renewal_date" +"%s")"
  ## Set full filename path using know directory structure and zonefile naming convention
  filename="/etc/bind/zones/db.$domainname"
  ## Get dane hash from zonefile
  danezone=$(exec cat $filename | grep "_443._tcp.$domainname" | cut -d " " -f7 | xargs)
  ## Calculate dane SHA256 hash based on the Letsencrypt certificate
  openssloutput=$(openssl x509 -in /etc/letsencrypt/live/www.$domainname/cert.pem -outform DER | openssl sha256)
  ## Get DANE hash from openssl output
  danecert=$(exec echo $openssloutput | cut -f2 -d "=" | xargs)
  ## Get current serial from zonefile
  currentserial=$(exec cat $filename | grep serial | cut -f1 -d ";" | xargs)
  ## Create new serial using today's date and add 00 at the end
  newserial="$(exec date +%Y%m%d)00"

  ## Now, let's check if certbot renewal (which is run every 12 hours on my Debian server) has resulted in a new certficate in the past hour
  if (( $current_epoch - $cert_file_renewal_epoch < 3600  )); then
    ## Certbot has renewed the certificate less than an hour ago; add DANE hash of new certificate to DNS zonefile

        	## Check if the DANE hash from the new certificate ($danecert) already exists in the zone file.
        	## If there are multiple DANE hashes for the root domain in the file, $danezone contains all hashes seperated by a single space
        	if [[ "$danezone" =~ .*$danecert.* ]]; then
                	## DANE hash already exists in the zone file
      ## Maybe this script is rerun for another domain and no action should be taken for the current domain
      ## Maybe you reused a key-pair

      ## Print status to stdout
                	echo [$domainname] DANE hash from new certificate already exists in zonefile
        	else
                  ## Find the lines for DANE and add a new DANE record.
      ## What I actually do: I replace the old DANE record with the old DANE record including a comment followed by a DANE record on a new line.
      ## I also only replace DANE records with certificate usage field '1'. That's how I make sure that the roll over DANE records (which have usage field '3') are left intact. 
                  sed -i "/_443._tcp.www.$domainname. IN TLSA 1/c\_443._tcp.www.$domainname. IN TLSA 1 0 1 $danezone ; old-dane-hash\n_443._tcp.www.$domainname. IN TLSA 1 0 1 $danecert" $filename
                  sed -i "/_443._tcp.$domainname. IN TLSA 1/c\_443._tcp.$domainname. IN TLSA 1 0 1 $danezone ; old-dane-hash\n_443._tcp.$domainname. IN TLSA 1 0 1 $danecert" $filename

                  ## Check whether to create new serial based on date of today, or increase current one, depending on which one is bigger
                  if (( $newserial > $currentserial )); then
                          ## Replace old serial with new serial
                          sed -i "s/$currentserial/$newserial/g" $filename
                  else
                          ## Add 1 to current serialnumber
                          sed -i "s/$currentserial/$((currentserial + 1))/g" $filename
                  fi

      ## Print status to stdout
      echo [$domainname] Added new DANE hash in zonefile
      ## DNSSEC: sign the changed zone
                  ods-signer sign $domainname
                  ## Reload bind
                  /etc/init.d/bind9 reload
    fi
  else
    ## Certificate was not renewed in the past hour (remember: this script should run every hour).
    ## Now I check if the certficate file is between 24 hours and 25 hours old. If that's the case then this means that the certificate was recently replaced and added to the DNS zone. 
    ## In my case 24 hours is more than enough time for the changed DNS zone (with the new DANE record) to spread over the internet. So after 24 hours, I activate the new certificate in apache and remove old certificate info from DNS zonefile
    if (( $current_epoch - $cert_file_renewal_epoch > 86400 )) && (( $current_epoch - $cert_file_renewal_epoch < 90000 )); then
      ## Check if the value 'old-dane-hash' exists in the zonefile
      if grep -qF old-dane-hash $filename; then
        ## Old DANE hashes exist in the zonefile; remove lines ending with the comment 'old-dane-hash'
        sed -i "/old-dane-hash/d" $filename

        ## Check whether to create new serial based on date of today, or increase current one, depending on which one is bigger
        	                if (( $newserial > $currentserial )); then
                                  ## Replace old serial with new serial
                                  sed -i "s/$currentserial/$newserial/g" $filename
        	                else
                	                ## Add 1 to current serialnumber
                        	        sed -i "s/$currentserial/$((currentserial + 1))/g" $filename
                        	fi

        ## Because I'm using custom symlinks for apache, now is the time to change the symlinks for this domain to the newest certificate files
        ln -sf $(readlink -f /etc/letsencrypt/live/www.$domainname/cert.pem) /etc/letsencrypt/live/www.$domainname/custom-cert.pem
        ln -sf $(readlink -f /etc/letsencrypt/live/www.$domainname/chain.pem) /etc/letsencrypt/live/www.$domainname/custom-chain.pem
        ln -sf $(readlink -f /etc/letsencrypt/live/www.$domainname/fullchain.pem) /etc/letsencrypt/live/www.$domainname/custom-fullchain.pem
        ln -sf $(readlink -f /etc/letsencrypt/live/www.$domainname/privkey.pem) /etc/letsencrypt/live/www.$domainname/custom-privkey.pem

        ## Print status to stdout
        echo [$domainname] Old DANE hashes removed from zonefile, and custom symlinks now point to new certificate

        ## Reload apache to activate new certificate
                        	/etc/init.d/apache2 reload

        ## DNSSEC: sign the changed zone
                        	ods-signer sign $domainname
                        	## Reload bind
                        	/etc/init.d/bind9 reload
                        else
                                ## No old DANE hashes exist in file; stop script and do nothing
        ## Maybe this domain was already processed in a previous run of this script

        ## Print status to stdout
        echo [$domainname] no old DANE hash found in zonefile
                        fi

    else
      if (( $current_epoch - $cert_file_renewal_epoch > 90000 )); then
        ## Do nothing, all done

        ## Print status to stdout
        echo [$domainname] The latest certificate file is over 25 hours old and not processed
      else
        ## The 24 hour wait required for DNS changes to spread across the internet, is not yet over. Be patient.

        ## Print status to stdout
        echo [$domainname] Be patient and wait at least 24 hours for the DNS changed to spread across the internet
      fi
    fi
  fi
done

 

2 antwoorden
  1. Marco zegt:

    Great to see people are working on this. Interesting approach with regard to rolling the TLSA records. Is it based on the NCSC factsheet perhaps? The method described in ‘https://tools.ietf.org/html/rfc6698#appendix-A.4′ is somewhat different, isn’t it?

    Instead of OpenDNSSEC one could also use BIND’s auto-maintain features for DNSSEC.

    Finally, I would recommend to consider using dynamic updates of the zone, for example by using the ‘nsupdate’ utility of BIND.

    Beantwoorden
  2. Dennis Baaten
    Dennis Baaten zegt:

    After having read appendix A.4 from RFC6698, you could say that this is actually what I’m dong with this script. There is also a similair concept in the NCSC factsheet. The nsupdate utility looks very interesting; I’m planning on taking a closer look at this tool. Nice tip!

    Beantwoorden

Plaats een Reactie

Meepraten?
Draag gerust bij!

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Deze website gebruikt Akismet om spam te verminderen. Bekijk hoe je reactie-gegevens worden verwerkt.