Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added --cross-profile to be able to handle Route53 and ELBs in different accounts #53

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,68 @@ An example IAM policy is:
]
}
```

### Cross-Account handling
You will need this feature, if Route53 is managed through your main account and ELB are provisioned in a separate AWS account.

Useful documentations: [AWS Examples of Policies for Delegating Access](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_policy-examples.html) and
[AWS Tutorial: Delegate Access Across AWS Accounts Using IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html)

In your account, which includes the ELB, you should create a new policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Action": [
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:SetLoadBalancerListenerSSLCertificate"
],
"Resource": [
"*"
]
},
{
"Sid": "",
"Effect": "Allow",
"Action": [
"iam:ListServerCertificates",
"iam:UploadServerCertificate",
"iam:DeleteServerCertificate",
"iam:GetServerCertificate"
],
"Resource": [
"*"
]
}
]
}
```
After this, you need to create a new `Role for Cross-Account Access`.
Enter your Account ID from your main account and choose the policy you've just created.
Edit the `Trust Relationships` and replace `arn:aws:iam::yourmainaccountnumber:root` with
`arn:aws:iam::yourmainaccountnumber:user/youruser`

In your main account, add a new policy to your user:
```json
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::yourELBaccountID:role/the-role-you-just-created"
}
}
```

Add `.aws/config` to your boto3 installation with the content:
```console
[profile crossaccount-example]
role_arn=arn:aws:iam::yourELBaccountID:role/the-role-you-just-created
source_profile=default
```

Then you can simply run it: `python letsencrypt-aws.py update-certificates --cross-profile=crossaccount-example`.

76 changes: 59 additions & 17 deletions letsencrypt-aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,42 @@ def update_certificate(self, logger, hosts, private_key, pem_certificate,


class Route53ChallengeCompleter(object):
def __init__(self, route53_client):
def __init__(self, route53_client, route53_cross_client=None):
self.route53_client = route53_client
self.route53_cross_client = route53_cross_client

# return the session where the DNS zone is located
def _find_client_for_domain(self, domain):
if not self.route53_cross_client:
return self.route53_client

for client in (self.route53_client, self.route53_cross_client):
paginator = client.get_paginator("list_hosted_zones")
for page in paginator.paginate():
for zone in page["HostedZones"]:
if (
domain.endswith(zone["Name"]) or
(domain + ".").endswith(zone["Name"])
) and not zone["Config"]["PrivateZone"]:
return client

raise ValueError(
"Unable to find a Route53 hosted zone for {}".format(domain)
)

def _find_zone_id_for_domain(self, domain):
paginator = self.route53_client.get_paginator("list_hosted_zones")
zones = []
for page in paginator.paginate():
for zone in page["HostedZones"]:
if (
domain.endswith(zone["Name"]) or
(domain + ".").endswith(zone["Name"])
) and not zone["Config"]["PrivateZone"]:
zones.append((zone["Name"], zone["Id"]))
for client in (self.route53_client, self.route53_cross_client):
if not client:
continue
paginator = client.get_paginator("list_hosted_zones")
for page in paginator.paginate():
for zone in page["HostedZones"]:
if (
domain.endswith(zone["Name"]) or
(domain + ".").endswith(zone["Name"])
) and not zone["Config"]["PrivateZone"]:
zones.append((zone["Name"], zone["Id"]))

if not zones:
raise ValueError(
Expand All @@ -147,7 +170,8 @@ def _find_zone_id_for_domain(self, domain):
return zones[0][1]

def _change_txt_record(self, action, zone_id, domain, value):
response = self.route53_client.change_resource_record_sets(
client = self._find_client_for_domain(domain)
response = client.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch={
"Changes": [
Expand Down Expand Up @@ -188,11 +212,12 @@ def delete_txt_record(self, change_id, host, value):
value
)

def wait_for_change(self, change_id):
def wait_for_change(self, change_id, host):
_, change_id = change_id
client = self._find_client_for_domain(host)

while True:
response = self.route53_client.get_change(Id=change_id)
response = client.get_change(Id=change_id)
if response["ChangeInfo"]["Status"] == "INSYNC":
return
time.sleep(5)
Expand Down Expand Up @@ -283,7 +308,10 @@ def complete_dns_challenge(logger, acme_client, dns_challenge_completer,
"updating-elb.wait-for-route53",
elb_name=elb_name, host=authz_record.host
)
dns_challenge_completer.wait_for_change(authz_record.change_id)
dns_challenge_completer.wait_for_change(
authz_record.change_id,
authz_record.host
)

response = authz_record.dns_challenge.response(acme_client.key)

Expand Down Expand Up @@ -467,7 +495,14 @@ def cli():
"expiration."
)
)
def update_certificates(persistent=False, force_issue=False):
@click.option(
"--cross-profile", help=(
"Specify your profile, if Route53 and ELB are in "
"different accounts located."
)
)
def update_certificates(persistent=False, force_issue=False,
cross_profile=False):
logger = Logger()
logger.emit("startup")

Expand All @@ -476,9 +511,16 @@ def update_certificates(persistent=False, force_issue=False):

session = boto3.Session()
s3_client = session.client("s3")
elb_client = session.client("elb")
route53_client = session.client("route53")
iam_client = session.client("iam")
if cross_profile:
cross_session = boto3.Session(profile_name=cross_profile)
elb_client = cross_session.client("elb")
iam_client = cross_session.client("iam")
route53_cross_client = cross_session.client("route53")
else:
elb_client = session.client("elb")
iam_client = session.client("iam")
route53_cross_client = None

config = json.loads(os.environ["LETSENCRYPT_AWS_CONFIG"])
domains = config["domains"]
Expand All @@ -504,7 +546,7 @@ def update_certificates(persistent=False, force_issue=False):

certificate_requests.append(CertificateRequest(
cert_location,
Route53ChallengeCompleter(route53_client),
Route53ChallengeCompleter(route53_client, route53_cross_client),
domain["hosts"],
domain.get("key_type", "rsa"),
))
Expand Down