diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..7ff097c3c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1 @@ +See LICENSE diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..4faaf21cd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE.txt +include README.md +recursive-include docs * +recursive-include letsencrypt_route53/tests/testdata * diff --git a/README.md b/README.md new file mode 100644 index 000000000..18725f1f7 --- /dev/null +++ b/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. Install the letsencrypt client [https://letsencrypt.readthedocs.org/en/latest/using.html#installation](https://letsencrypt.readthedocs.org/en/latest/using.html#installation) + + ``` + pip install letsencrypt + ``` + +1. Install the letsencrypt-s3front plugin + + ``` + pip install letsencrypt-s3front + ``` + +### How to use it + +To generate a certificate and install it in a CloudFront distribution: +``` +AWS_ACCESS_KEY_ID="your_key" \ +AWS_SECRET_ACCESS_KEY="your_secret" \ +letsencrypt --agree-tos -a letsencrypt-route53:auth \ +-d the_domain +``` + +Follow the screen prompts and you should end up with the certificate in your +distribution. It may take a couple minutes to update. + +To automate the renewal process without prompts (for example, with a monthly cron), you can add the letsencrypt parameters --renew-by-default --text diff --git a/letsencrypt_route53/__init__.py b/letsencrypt_route53/__init__.py new file mode 100644 index 000000000..6f519023b --- /dev/null +++ b/letsencrypt_route53/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt Route53 plugin.""" diff --git a/letsencrypt_route53/authenticator.py b/letsencrypt_route53/authenticator.py new file mode 100644 index 000000000..280a5bb55 --- /dev/null +++ b/letsencrypt_route53/authenticator.py @@ -0,0 +1,93 @@ +"""Route53 Let's Encrypt authenticator plugin.""" +import os +import logging +import re +import subprocess + +import zope.component +import zope.interface + +import boto3 + +from acme import challenges + +from letsencrypt import errors +from letsencrypt import interfaces +from letsencrypt.plugins import common + + +logger = logging.getLogger(__name__) + +class Authenticator(common.Plugin): + zope.interface.implements(interfaces.IAuthenticator) + zope.interface.classProvides(interfaces.IPluginFactory) + + description = "Route53 Authenticator" + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self._httpd = None + + 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 ("") + + def get_chall_pref(self, domain): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return [challenges.DNS] + + def perform(self, achalls): # pylint: disable=missing-docstring + responses = [] + for achall in achalls: + responses.append(self._perform_single(achall)) + return responses + + def _perform_single(self, achall): + # provision the TXT record, using the domain name given. Assumes the hosted zone exits, else fails the challenge + response, validation = achall.response_and_validation() + r53 = boto3.client('route53') + logger.info("Doing validation for " + achall) + listResponse = r53.list_hosted_zones_by_name(DNSName=achall.chall.path[1:]) + matches = listResponse.HostedZones; + if matches.size != 0: + logger.error("Route53 returned " + mathces.size + " matching hosted zones. Expected exactly one. Auth canceled.") + return None + else: + r53.change_resource_record_sets(HostedZoneId=matches[0].Id, + ChangeBatch={ + 'Comment': 'Let\'s Entcrypt Change', + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': achall.chall.path[1:], + 'Type': 'TXT', + 'TTL': 300, + 'ResourceRecords': [ + { + 'Value': validation + }, + ] + } + }, + ] + }) + + if response.simple_verify( + achall.chall, achall.domain, + achall.account_key.public_key(), self.config.http01_port): + return response + else: + logger.error( + "Self-verify of challenge failed, authorization abandoned!") + return None + + def cleanup(self, achalls): + # pylint: disable=missing-docstring,no-self-use,unused-argument + #TODO:Cleanup recordĀ  + r53 = boto3.client('route53') + #for achall in achalls: + # r53.delete_object(Bucket=self.conf('s3-bucket'), Key=achall.chall.path[1:]) + return None diff --git a/sample-aws-policy.json b/sample-aws-policy.json new file mode 100644 index 000000000..1af66c85d --- /dev/null +++ b/sample-aws-policy.json @@ -0,0 +1,36 @@ +{ + "Version": "2012-10-17", + "Id": "letsencrypt-route53 sample policy", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:UploadServerCertificate", + "iam:UpdateServerCertificate", + "iam:DeleteServerCertificate" + ], + "Resource": [ + "*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "route53:List*", + "route53:Get*", + ], + "Resource": [ + "*" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "route53:ChangeResourceRecordSets" + ], + "Resource" : [ + "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID" + ] + } + ] +} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..b88034e41 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..5b2e1da06 --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +import sys + +from distutils.core import setup +from setuptools import find_packages + +version = '0.1.3' + +install_requires = [ + 'acme>=0.1.1', + 'letsencrypt>=0.1.1', + 'PyOpenSSL', + 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? + 'setuptools', # pkg_resources + 'zope.interface', + 'boto3' +] + +if sys.version_info < (2, 7): + install_requires.append('mock<1.1.0') +else: + install_requires.append('mock') + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='letsencrypt-route53', + version=version, + description="Route53 plugin for Let's Encrypt client", + url='https://github.com/mindstorms6/letsencrypt-route53', + author="Breland Miley", + author_email='breland@bdawg.org', + 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.6', + '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(), + include_package_data=True, + install_requires=install_requires, + keywords = ['letsencrypt', 'route53', 'aws'], + entry_points={ + 'letsencrypt.plugins': [ + 'auth = letsencrypt_s3front.authenticator:Authenticator' + ], + }, +)