diff --git a/certbot-route53/.gitignore b/certbot-route53/.gitignore new file mode 100644 index 000000000..1dbc687de --- /dev/null +++ b/certbot-route53/.gitignore @@ -0,0 +1,62 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/certbot-route53/LICENSE b/certbot-route53/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/certbot-route53/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/certbot-route53/MANIFEST.in b/certbot-route53/MANIFEST.in new file mode 100644 index 000000000..9575a1c62 --- /dev/null +++ b/certbot-route53/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include README diff --git a/certbot-route53/README.md b/certbot-route53/README.md new file mode 100644 index 000000000..cec9c295c --- /dev/null +++ b/certbot-route53/README.md @@ -0,0 +1,35 @@ +## Route53 plugin for Let's Encrypt client + +### Before you start + +It's expected that the root hosted zone for the domain in question already +exists in your account. + +### Setup + +1. Create a virtual environment + +2. Update its pip and setuptools (`VENV/bin/pip install -U setuptools pip`) +to avoid problems with cryptography's dependency on setuptools>=11.3. + +3. Make sure you have libssl-dev and libffi (or your regional equivalents) +installed. You might have to set compiler flags to pick things up (I have to +use `CPPFLAGS=-I/usr/local/opt/openssl/include +LDFLAGS=-L/usr/local/opt/openssl/lib` on my macOS to pick up brew's openssl, +for example). + +4. Install this package. + +### How to use it + +Make sure you have access to AWS's Route53 service, either through IAM roles or +via `.aws/credentials`. Check out +[sample-aws-policy.json](sample-aws-policy.json) for the necessary permissions. + +To generate a certificate: +``` +certbot certonly \ + -n --agree-tos --email DEVOPS@COMPANY.COM \ + -a certbot-route53:auth \ + -d MY.DOMAIN.NAME +``` diff --git a/certbot-route53/certbot_route53/__init__.py b/certbot-route53/certbot_route53/__init__.py new file mode 100644 index 000000000..c91c79c22 --- /dev/null +++ b/certbot-route53/certbot_route53/__init__.py @@ -0,0 +1 @@ +"""Certbot Route53 plugin.""" diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-route53/certbot_route53/authenticator.py new file mode 100644 index 000000000..855165455 --- /dev/null +++ b/certbot-route53/certbot_route53/authenticator.py @@ -0,0 +1,148 @@ +"""Certbot Route53 authenticator plugin.""" +import logging +import time +import datetime + +import zope.interface + +import boto3 +from botocore.exceptions import NoCredentialsError, ClientError + +from acme import challenges + +from certbot import interfaces +from certbot.plugins import common + + +logger = logging.getLogger(__name__) + +TTL = 10 + +INSTRUCTIONS = ( + "To use certbot-route53, configure credentials as described at " + "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " + "and add the necessary permissions for Route53 access.") + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(common.Plugin): + """Route53 Authenticator + + This authenticator solves a DNS01 challenge by uploading the answer to AWS + Route53. + """ + + description = ("Authenticate domain names using the DNS challenge type, " + "by automatically updating TXT records using AWS Route53. Works only " + "if you use AWS Route53 to host DNS for your domains. " + + INSTRUCTIONS) + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.r53 = boto3.client("route53") + + def prepare(self): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return "Solve a DNS01 challenge using AWS Route53" + + def get_chall_pref(self, domain): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return [challenges.DNS01] + + def perform(self, achalls): # pylint: disable=missing-docstring + try: + change_ids = [ + self._change_txt_record("UPSERT", achall) + for achall in achalls + ] + + for change_id in change_ids: + self._wait_for_change(change_id) + # Sleep for at least the TTL, to ensure that any records cached by + # the ACME server after previous validation attempts are gone. In + # most cases we'll need to wait at least this long for the Route53 + # records to propagate, so this doesn't delay us much. + time.sleep(TTL) + return [achall.response(achall.account_key) for achall in achalls] + except (NoCredentialsError, ClientError) as e: + e.args = ("\n".join([str(e), INSTRUCTIONS]),) + raise + + def cleanup(self, achalls): # pylint: disable=missing-docstring + for achall in achalls: + self._change_txt_record("DELETE", achall) + + def _find_zone_id_for_domain(self, domain): + """Find the zone id responsible a given FQDN. + + That is, the id for the zone whose name is the longest parent of the + domain. + """ + paginator = self.r53.get_paginator("list_hosted_zones") + zones = [] + target_labels = domain.rstrip(".").split(".") + for page in paginator.paginate(): + for zone in page["HostedZones"]: + if zone["Config"]["PrivateZone"]: + continue + + candidate_labels = zone["Name"].rstrip(".").split(".") + if candidate_labels == target_labels[-len(candidate_labels):]: + zones.append((zone["Name"], zone["Id"])) + + if not zones: + raise ValueError( + "Unable to find a Route53 hosted zone for {}".format(domain) + ) + + # Order the zones that are suffixes for our desired to domain by + # length, this puts them in an order like: + # ["foo.bar.baz.com", "bar.baz.com", "baz.com", "com"] + # And then we choose the first one, which will be the most specific. + zones.sort(key=lambda z: len(z[0]), reverse=True) + return zones[0][1] + + def _change_txt_record(self, action, achall): + domain = achall.validation_domain_name(achall.domain) + value = achall.validation(achall.account_key) + + zone_id = self._find_zone_id_for_domain(domain) + + response = self.r53.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "certbot-route53 certificate validation " + action, + "Changes": [ + { + "Action": action, + "ResourceRecordSet": { + "Name": domain, + "Type": "TXT", + "TTL": TTL, + "ResourceRecords": [ + # For some reason TXT records need to be + # manually quoted. + {"Value": '"{}"'.format(value)} + ], + } + } + ] + } + ) + return response["ChangeInfo"]["Id"] + + def _wait_for_change(self, change_id): + """Wait for a change to be propagated to all Route53 DNS servers. + https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html + """ + client = boto3.client("route53") + for n in range(0, 120): + response = client.get_change(Id=change_id) + if response["ChangeInfo"]["Status"] == "INSYNC": + return + time.sleep(5) + raise Exception( + "Timed out waiting for Route53 change. Current status: %s" % + response["ChangeInfo"]["Status"]) diff --git a/certbot-route53/sample-aws-policy.json b/certbot-route53/sample-aws-policy.json new file mode 100644 index 000000000..0b4dcae41 --- /dev/null +++ b/certbot-route53/sample-aws-policy.json @@ -0,0 +1,25 @@ +{ + "Version": "2012-10-17", + "Id": "certbot-route53 sample policy", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZones", + "route53:GetChange" + ], + "Resource": [ + "*" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "route53:ChangeResourceRecordSets" + ], + "Resource" : [ + "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID" + ] + } + ] +} diff --git a/certbot-route53/setup.cfg b/certbot-route53/setup.cfg new file mode 100644 index 000000000..3c6e79cf3 --- /dev/null +++ b/certbot-route53/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/certbot-route53/setup.py b/certbot-route53/setup.py new file mode 100644 index 000000000..49b1ea467 --- /dev/null +++ b/certbot-route53/setup.py @@ -0,0 +1,47 @@ +import sys + +from distutils.core import setup +from setuptools import find_packages + +version = '0.1.5' + +install_requires = [ + 'acme>=0.9.0', + 'certbot>=0.9.0', + 'zope.interface', + 'boto3', +] + +setup( + name='certbot-route53', + version=version, + description="Route53 plugin for certbot", + url='https://github.com/lifeonmarspt/certbot-route53', + author="Hugo Peixoto", + author_email='hugo@lifeonmars.pt', + license='Apache2.0', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + packages=find_packages(), + install_requires=install_requires, + keywords=['certbot', 'route53', 'aws'], + entry_points={ + 'certbot.plugins': [ + 'auth = certbot_route53.authenticator:Authenticator' + ], + }, +) diff --git a/certbot-route53/tester.pkoch-macos_sierra.sh b/certbot-route53/tester.pkoch-macos_sierra.sh new file mode 100755 index 000000000..dbbaa2251 --- /dev/null +++ b/certbot-route53/tester.pkoch-macos_sierra.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# I just wanted a place to dump the incantations I use for testing. +set -e + +brew install openssl libffi + +rm -rf scratch; mkdir scratch + +virtualenv scratch/venv -p /usr/local/bin/python2.7 +scratch/venv/bin/pip install -U pip setuptools + +CPPFLAGS=-I/usr/local/opt/openssl/include LDFLAGS=-L/usr/local/opt/openssl/lib scratch/venv/bin/pip install -e . + +scratch/venv/bin/certbot certonly -n --agree-tos --test-cert --email pkoch@lifeonmars.pt -a certbot-route53:auth -d pkoch.lifeonmars.pt --work-dir scratch --config-dir scratch --logs-dir scratch + +rm -rf scratch