Skip to content

Instantly share code, notes, and snippets.

@rmarchei
Last active June 16, 2022 04:11
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save rmarchei/98489c05f0898abe612eec916508f2bf to your computer and use it in GitHub Desktop.
Save rmarchei/98489c05f0898abe612eec916508f2bf to your computer and use it in GitHub Desktop.
route53 hook for dehydrated - python2 / python3 + boto2 version. Tested on Ubuntu 16.04
#!/usr/bin/env python
# How to use:
#
# Ubuntu 16.04: apt install -y python-boto OR apt install -y python3-boto
#
# Specify the default profile on aws/boto profile files or use the optional AWS_PROFILE env var:
# AWS_PROFILE=example ./dehydrated -c -d example.com -t dns-01 -k /etc/dehydrated/hooks/route53.py
#
# Manually specify hosted zone:
# HOSTED_ZONE=example.com AWS_PROFILE=example ./dehydrated -c -d example.com -t dns-01 -k /etc/dehydrated/hooks/route53.py
#
# More info about dehaydrated and dns challenge: https://github.com/lukas2511/dehydrated/wiki/Examples-for-DNS-01-hooks
# Using AWS Profiles: http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-multiple-profiles
import os
import sys
from boto.route53 import *
from time import sleep
def route53_dns(domain, txt_challenge, action='upsert'):
conn = connection.Route53Connection()
action = action.upper()
if 'HOSTED_ZONE' in os.environ:
hosted_zone = os.environ['HOSTED_ZONE']
if not domain.endswith(hosted_zone):
raise Exception("Incorrect hosted zone for domain {0}".format(domain))
zone = conn.get_hosted_zone_by_name("{0}.".format(hosted_zone))
zone_id = zone['GetHostedZoneResponse']['HostedZone']['Id'].replace('/hostedzone/', '')
else:
zones = conn.get_all_hosted_zones()
for zone in zones['ListHostedZonesResponse']['HostedZones']:
if "{0}.".format(domain).endswith(zone['Name']):
zone_id = zone['Id'].replace('/hostedzone/', '')
break
else:
raise Exception("Hosted zone not found for domain {0}".format(domain))
name = u'_acme-challenge.{0}.'.format(domain)
record_set = conn.get_all_rrsets(zone_id, name=name, type='TXT')
challenges = [u'"{0}"'.format(txt_challenge)]
for r in record_set:
if r.name == name and r.type == 'TXT':
challenges += r.resource_records
change_set = record.ResourceRecordSets(conn, zone_id)
change = change_set.add_change("{0}".format(action), '_acme-challenge.{0}'.format(domain), type='TXT', ttl=60)
for c in set(challenges):
change.add_value(c)
try:
response = change_set.commit()
except Exception as e:
if action == "DELETE":
pass
else:
print(e.message, e.args)
if action in ('CREATE', 'UPSERT'):
# wait for DNS update
timeout = 300
sleep_time = 5
time_elapsed = 0
st = status.Status(conn, response['ChangeResourceRecordSetsResponse']['ChangeInfo'])
while st.update() != 'INSYNC' and time_elapsed <= timeout:
print("Waiting for DNS change to complete... (Elapsed {0} seconds)".format(time_elapsed))
sleep(sleep_time)
time_elapsed += sleep_time
if st.update() != 'INSYNC' and time_elapsed > timeout:
raise Exception("Timed out while waiting for DNS record to be ready. Waited {0} seconds".format(time_elapsed))
print("DNS change completed")
if __name__ == "__main__":
hook = sys.argv[1]
if len(sys.argv) > 2:
domain = sys.argv[2]
txt_challenge = sys.argv[4]
else:
domain = None
txt_challenge = None
if hook == "deploy_challenge":
action = 'upsert'
elif hook == "clean_challenge":
action = 'delete'
else:
sys.exit(0)
print("hook: {0}".format(hook))
print("domain: {0}".format(domain))
print("txt_challenge: {0}".format(txt_challenge))
route53_dns(domain, txt_challenge, action)
@nh2
Copy link

nh2 commented May 27, 2018

I've written a fork of this gist to:

  • Implement wildcard support
  • Allow HOOK_CHAIN="yes" for much faster batch confirmation (only need to wait for DNS propagation once, not once per domain)

https://gist.github.com/nh2/f744ac591e95f0c25b501db00cf7c71a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment