From fd4bdbdf6f3f1c801147766dbd0c5c41688c9031 Mon Sep 17 00:00:00 2001 From: Kristian Lewis Jones Date: Mon, 25 Jan 2016 15:36:41 +0000 Subject: [PATCH] Adds --cert-only and --create-listener flags The --cert-only flag will create the certificate and does not attempt to add the certificate to the ELB. This would allow you to then create the listener manually (or some other method) with the new certificate. The --create-listener flag will create the listener for the certificate if it doesn't exist. --- README.md | 14 +++++- letsencrypt-aws.py | 121 +++++++++++++++++++++++++++++++-------------- 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 1ea7f1a..a3f42c6 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,12 @@ environment variable. This should be a JSON object with the following schema: { "elb": { "name": "ELB name (string)", - "port": "optional, defaults to 443 (integer)" + "listener": { + "load_balancer_port": "optional, defaults to 443 (integer)", + "protocol": "optional, used with --create-listener flag", + "instance_protocol": "optional, used with --create-listener flag", + "instance_port": "optional, used with --create-listener flag" + } }, "hosts": ["list of hosts you want on the certificate (strings)"], "key_type": "rsa or ecdsa, optional, defaults to rsa (string)" @@ -117,6 +122,10 @@ If your `acme_account_key` is provided as an `s3://` URI you will also need: * `s3:GetObject` +If you want to use the `--create-listener` flag you will also need: + +* `elasticloadbalancing:CreateLoadBalancerListeners` + It's likely possible to restrict these permissions by ARN, though this has not been fully explored. @@ -144,7 +153,8 @@ An example IAM policy is: "Effect": "Allow", "Action": [ "elasticloadbalancing:DescribeLoadBalancers", - "elasticloadbalancing:SetLoadBalancerListenerSSLCertificate" + "elasticloadbalancing:SetLoadBalancerListenerSSLCertificate", + "elasticloadbalancing:CreateLoadBalancerListeners" ], "Resource": [ "*" diff --git a/letsencrypt-aws.py b/letsencrypt-aws.py index 59cb998..0547d75 100644 --- a/letsencrypt-aws.py +++ b/letsencrypt-aws.py @@ -21,6 +21,7 @@ import rfc3986 +import botocore DEFAULT_ACME_DIRECTORY_URL = "https://acme-v01.api.letsencrypt.org/directory" CERTIFICATE_EXPIRATION_THRESHOLD = datetime.timedelta(days=45) @@ -133,17 +134,18 @@ def generate_certificate_name(hosts, cert): ) -def get_load_balancer_certificate(elb_client, elb_name, elb_port): +def get_load_balancer_certificate(elb_client, elb_name, listener): + elb_port = listener.get("load_balancer_port", 443) response = elb_client.describe_load_balancers( LoadBalancerNames=[elb_name] ) [description] = response["LoadBalancerDescriptions"] - [certificate_id] = [ - listener["Listener"]["SSLCertificateId"] - for listener in description["ListenerDescriptions"] - if listener["Listener"]["LoadBalancerPort"] == elb_port - ] - return certificate_id + + for listener in description["ListenerDescriptions"]: + if listener["Listener"]["LoadBalancerPort"] == elb_port: + return listener["Listener"]["SSLCertificateId"] + + return False def get_expiration_date_for_certificate(iam_client, ssl_certificate_arn): @@ -245,9 +247,9 @@ def request_certificate(logger, acme_client, elb_name, authorizations, csr): return pem_certificate, pem_certificate_chain -def add_certificate_to_elb(logger, elb_client, iam_client, elb_name, elb_port, +def add_certificate_to_elb(logger, elb_client, iam_client, elb_name, listener, hosts, private_key, pem_certificate, - pem_certificate_chain): + pem_certificate_chain, cert_only, create_listener): logger.emit("updating-elb.upload-iam-certificate", elb_name=elb_name) response = iam_client.upload_server_certificate( ServerCertificateName=generate_certificate_name( @@ -264,37 +266,65 @@ def add_certificate_to_elb(logger, elb_client, iam_client, elb_name, elb_port, ) new_cert_arn = response["ServerCertificateMetadata"]["Arn"] + if cert_only: + return + # Sleep before trying to set the certificate, it appears to sometimes fail # without this. time.sleep(15) logger.emit("updating-elb.set-elb-certificate", elb_name=elb_name) - elb_client.set_load_balancer_listener_ssl_certificate( - LoadBalancerName=elb_name, - SSLCertificateId=new_cert_arn, - LoadBalancerPort=elb_port, - ) + elb_port = listener.get("load_balancer_port", 443) + try: + logger.emit("updating-elb.update-listener", elb_name=elb_name) + elb_client.set_load_balancer_listener_ssl_certificate( + LoadBalancerName=elb_name, + SSLCertificateId=new_cert_arn, + LoadBalancerPort=elb_port, + ) + except botocore.exceptions.ClientError as e: + if 'ListenerNotFound' not in str(e): + raise e + + if not create_listener: + raise e + + logger.emit("updating-elb.create-listener", elb_name=elb_name) + elb_client.create_load_balancer_listeners( + LoadBalancerName=elb_name, + Listeners=[ + { + 'Protocol': listener['protocol'], + 'LoadBalancerPort': elb_port, + 'InstanceProtocol': listener['instance_protocol'], + 'InstancePort': listener['instance_port'], + 'SSLCertificateId': new_cert_arn, + } + ] + ) def update_elb(logger, acme_client, elb_client, route53_client, iam_client, - force_issue, elb_name, elb_port, hosts, key_type): + force_issue, elb_name, listener, hosts, key_type, cert_only, + create_listener): logger.emit("updating-elb", elb_name=elb_name) certificate_id = get_load_balancer_certificate( - elb_client, elb_name, elb_port + elb_client, elb_name, listener ) - expiration_date = get_expiration_date_for_certificate( - iam_client, certificate_id - ).date() - logger.emit( - "updating-elb.certificate-expiration", - elb_name=elb_name, expiration_date=expiration_date - ) - days_until_expiration = expiration_date - datetime.date.today() - if ( - days_until_expiration > CERTIFICATE_EXPIRATION_THRESHOLD and - not force_issue - ): - return + if certificate_id: + expiration_date = get_expiration_date_for_certificate( + iam_client, certificate_id + ).date() + logger.emit( + "updating-elb.certificate-expiration", + elb_name=elb_name, expiration_date=expiration_date + ) + days_until_expiration = expiration_date - datetime.date.today() + if ( + days_until_expiration > CERTIFICATE_EXPIRATION_THRESHOLD and + not force_issue + ): + return if key_type == "rsa": private_key = generate_rsa_private_key() @@ -324,8 +354,9 @@ def update_elb(logger, acme_client, elb_client, route53_client, iam_client, add_certificate_to_elb( logger, elb_client, iam_client, - elb_name, elb_port, hosts, - private_key, pem_certificate, pem_certificate_chain + elb_name, listener, hosts, + private_key, pem_certificate, pem_certificate_chain, + cert_only, create_listener ) finally: for authz_record in authorizations: @@ -344,7 +375,7 @@ def update_elb(logger, acme_client, elb_client, route53_client, iam_client, def update_elbs(logger, acme_client, elb_client, route53_client, iam_client, - force_issue, domains): + force_issue, domains, cert_only, create_listener): for domain in domains: update_elb( logger, @@ -354,9 +385,11 @@ def update_elbs(logger, acme_client, elb_client, route53_client, iam_client, iam_client, force_issue, domain["elb"]["name"], - domain["elb"].get("port", 443), + domain["elb"].get("listener", {'load_balancer_port': 443}), domain["hosts"], - domain.get("key_type", "rsa") + domain.get("key_type", "rsa"), + cert_only, + create_listener ) @@ -402,7 +435,19 @@ def cli(): "expiration." ) ) -def update_certificates(persistent=False, force_issue=False): +@click.option( + "--cert-only", is_flag=True, help=( + "Only issue the certificate. Do not attempt to add the certificate " + "to the ELB." + ) +) +@click.option( + "--create-listener", is_flag=True, help=( + "Create the HTTPS listener if it is missing." + ) +) +def update_certificates(persistent=False, force_issue=False, + cert_only=False, create_listener=False): logger = Logger() logger.emit("startup") @@ -417,7 +462,7 @@ def update_certificates(persistent=False, force_issue=False): # Structure: { # "domains": [ - # {"elb": {"name" "...", "port" 443}, hosts: ["..."]} + # {"elb": {"name" "...", "listener": { ... }}, hosts: ["..."]} # ], # "acme_account_key": "s3://bucket/object", # "acme_directory_url": "(optional)" @@ -437,7 +482,7 @@ def update_certificates(persistent=False, force_issue=False): while True: update_elbs( logger, acme_client, elb_client, route53_client, iam_client, - force_issue, domains + force_issue, domains, cert_only, create_listener ) # Sleep before we check again logger.emit("sleeping", duration=PERSISTENT_SLEEP_INTERVAL) @@ -446,7 +491,7 @@ def update_certificates(persistent=False, force_issue=False): logger.emit("running", mode="single") update_elbs( logger, acme_client, elb_client, route53_client, iam_client, - force_issue, domains + force_issue, domains, cert_only, create_listener )