Unsupported: lab only
This is a community script, not something Broadcom supports. Run it against the Let’s Encrypt staging environment first and make sure you understand what it does before you point it at anything that matters.
I run several domains behind VMware Avi Load Balancer (still NSX ALB to a lot of us), and for a while the certificates were a patchwork. A few Cloudflare Origin certificates, a Let’s Encrypt certificate I had renewed by hand, and one that had quietly expired months ago without anyone noticing. I wanted one thing: a wildcard certificate per domain that renews itself, issued and stored on the Avi Controller, with Cloudflare handling the DNS validation.
Avi already has a framework for exactly this, called Certificate Management. It just does not ship with a Cloudflare option. This post walks through the script I wrote to fill that gap, how to set it up, and the handful of problems I hit along the way.
Why the stock scripts do not cover it
Let’s Encrypt only issues wildcard certificates through the DNS-01 challenge. You prove you own the domain by publishing a TXT record at _acme-challenge.example.com. The default Avi integration uses HTTP-01, which validates through a virtual service on port 80, so it is out for wildcards straight away.
Broadcom does publish DNS-01 scripts in the avinetworks/devops repository, but only for Infoblox and AWS Route 53. Both of them also look up a virtual service by FQDN before they do anything else. No virtual service has the name *.example.com, so even if you swapped in Cloudflare calls, a wildcard request would fail at that lookup.
How it works
A Certificate Management profile points at a ControlScript. When you create a certificate of type CSR and attach the profile, the Controller generates the private key and the CSR, then runs the script and hands it the CSR. The script talks to Let’s Encrypt, completes the challenge, and returns the signed certificate, which the Controller imports. The private key never leaves Avi.
For DNS-01 against Cloudflare, the script does this:
- Registers (or reuses) an ACME account and creates an order for every name in the CSR.
- Publishes one TXT record per name through the Cloudflare API.
- Waits for DNS to settle, then tells Let’s Encrypt the challenges are ready.
- Polls until every authorization is valid, finalizes the order and downloads the certificate.
- Deletes the TXT records, whether validation passed or not.
The script runs on the Controller itself, on the cluster leader if you have three nodes, not on the Service Engines. Everything is outbound. The Controller management network needs HTTPS to acme-v02.api.letsencrypt.org and api.cloudflare.com, and nothing has to reach in. Renewal is handled by Avi: it runs the script again ahead of expiry on its own.
Compared with the Infoblox script it started from, I made three structural changes:
- The Infoblox calls are replaced with Cloudflare API v4 calls, authenticated with a scoped API token.
- The virtual service lookup is gone. DNS-01 does not need it, and removing it means the script needs no Avi username or password at all.
- All TXT records are published before any challenge is triggered. More on why in the gotchas section.
Step 1: Create a Cloudflare API token
In the Cloudflare dashboard go to My Profile, API Tokens, Create Token, and start from the Edit zone DNS template. Under Zone Resources choose Include, All zones from an account if you want one token to cover every domain you own, or pick a single zone if you prefer to keep it tight.
Add a second permission, Zone / Zone / Read. The script finds the right zone by name, and that lookup needs read access to zones. If you would rather not grant it, you can pass the zone ID directly with the cf_zone_id parameter, but then the profile only works for that one zone.
Step 2: Upload the ControlScript
In Avi, go to Templates > Scripts > ControlScripts and click Create. Give it a name such as letsencrypt-cloudflare-dns and paste in the full script from the end of this post.
Step 3: Create the Certificate Management profile
Go to Templates > Security > Certificate Management, click Create, and select the ControlScript you just made. Under Custom Script Params add the following. The names must match exactly.
| Name | Value | Sensitive |
|---|---|---|
cf_api_token | Your Cloudflare API token | Yes |
dryrun | true while testing | No |
cf_zone_id | Optional, only to pin the profile to one zone | No |
cf_propagation_wait | Optional, seconds to wait after publishing (default 30) | No |
Leave Dynamic off for all of them. Dynamic means Avi asks for the value on every certificate, which is not what you want here.
You will also see a contact parameter in the script. Let’s Encrypt stopped storing contact email addresses, so it does nothing useful anymore. I left it supported but you can skip it.
Step 4: Request the certificate
Go to Templates > Security > SSL/TLS Certificates, click Create, then Application Certificate, and fill it in like this:
- Type: CSR
- Common Name:
*.example.com - Subject Alternate Names:
*.example.comandexample.com - Algorithm: EC or RSA, whichever you prefer
- Certificate Management Profile: the profile from step 3
Save it and give it a minute or two. Most of that time is the propagation wait. With dryrun set to true the certificate comes from the Let’s Encrypt staging CA, so the issuer will read something like (STAGING) Artificial Amaranth YE1 and Avi will flag it as untrusted. That is expected. It proves the whole flow works without burning production rate limits.
Once staging works, set dryrun to false on the profile, delete the test certificate, and create it again. Repeat for each domain. One certificate per domain is cleaner than one huge certificate with every domain in it: a single failing domain cannot block renewal of the others, and you are not advertising your whole domain list to anyone who connects.
If a request fails, the script output is shown right on the certificate in the Avi UI, which makes troubleshooting easy.
Step 5: Import the issuing intermediate
My first production certificates showed an orange warning in the certificate list with the tooltip Issuer Certificate Missing. Let’s Encrypt now issues from newer intermediates (mine came from YR1 and YR2), and Avi did not have them. Without the intermediate, Avi cannot build the chain, and the SSL score on any virtual service using the certificate drops.
The quickest fix is to read the issuer’s download address out of the certificate itself. Export the certificate text from Avi into a file called wildcard.crt on any Linux box, then run:
AIA=$(openssl x509 -in wildcard.crt -noout -text | grep -o 'CA Issuers - URI:.*' | sed 's/.*URI://')
curl -s "$AIA" | openssl x509 -inform DER -out intermediate.pem
openssl x509 -in intermediate.pem -noout -subject -issuer
cat intermediate.pem
Import the PEM output under Root/Intermediate CA Certificate and the warnings clear. If the issuer line shows a root Avi does not already have, fetch and import that one the same way.
Bonus: exporting a PFX
Avi only exports PEM, but if you need a PFX for a Windows box or an appliance it takes one command. Use the export option on the certificate row to get the key and certificate text, save them as wildcard.key and wildcard.crt, fetch intermediate.pem as shown above, and run:
openssl pkcs12 -export -inkey wildcard.key -in wildcard.crt -certfile intermediate.pem \
-name "wildcard-example-com" -out wildcard-example-com.pfx
If you build the files by pasting into cat > file over SSH, finish with Enter and then Ctrl+D. Ctrl+C drops the last line, which is the END line of the PEM, and OpenSSL will refuse to read the file. Keep in mind a PFX is a snapshot. Avi renews its own copy automatically, but anything you installed the PFX on keeps the old certificate until you export it again.
Wrapping up
With this in place every domain I own has a wildcard certificate that Avi renews on its own, the private keys never leave the Controller, and the only credential involved is a scoped Cloudflare token. If you adapt it for another DNS provider, the only functions you need to touch are add_dns_text_record and remove_dns_text_record.
The full script
letsencrypt_mgmt_profile_cloudflare_dns.py
'''
###
# Name: letsencrypt_mgmt_profile_cloudflare_dns.py
# Version: 0.1.2-cloudflare
# License: MIT
#
# Description -
# Certificate Management Profile script for VMware Avi Load Balancer that requests
# and renews certificates from Let's Encrypt using the DNS-01 challenge against a
# Cloudflare-hosted zone. Because it uses DNS-01 it can issue wildcard certificates
# (*.domain.tld) and needs no inbound connectivity and no Virtual Service.
#
# Adapted from the official Infoblox DNS-01 script:
# https://github.com/avinetworks/devops/blob/master/certificate_mgmt/letsencrypt_mgmt_profile_infoblox_dns.py
# Changes from that script:
# - Infoblox calls replaced with Cloudflare API v4 calls.
# - Virtual Service lookup removed (it is only needed for HTTP-01 and it fails for
# wildcard names), so no Avi user/password is required.
# - Waits for DNS propagation before asking Let's Encrypt to validate.
#
# Setup -
# 1. Templates > Security > Certificate Management > Create, paste this script.
# 2. Add the script params listed below (mark cf_api_token as sensitive).
# 3. Templates > Security > SSL/TLS Certificates > Create > Application Certificate.
# 4. Type: CSR. Common Name: *.domain.tld. Add domain.tld as a SAN if you want the
# apex covered as well. Select this Certificate Management profile.
# 5. Save. Issuance takes roughly a minute.
#
# Parameters -
# cf_api_token - Cloudflare API token with Zone / DNS / Edit on the zone. (required, sensitive)
# cf_zone_id - Cloudflare Zone ID. (recommended; if omitted the script looks the
# zone up by name, which needs Zone / Zone / Read on the token)
# cf_propagation_wait - Seconds to wait after creating the TXT records. (Default: 30)
# cf_ttl - TTL of the TXT record in seconds, minimum 60. (Default: 60)
# dryrun - True/False. If True Let's Encrypt's staging server is used. (Default: False)
# Use this while testing so you do not hit rate limits.
# contact - E-mail address sent to Let's Encrypt for the account. (Default: None)
# directory_url - Use a different ACME server. (Default: Let's Encrypt Production)
# letsencrypt_key - Existing Let's Encrypt account key in PEM. (Default: None, one is generated)
# debug - True/False. Extra logging. (Default: False)
#
# Authors/Credits -
# acme-tiny <https://github.com/diafygi/acme-tiny>
# acme-dns-tiny <https://github.com/Trim/acme-dns-tiny>
# Nikhil Kumar Yadav, Patrik Kernstock, Priya Koshta (VMware) for the original Avi scripts
###
'''
import base64, binascii, hashlib, os, json, re, ssl, subprocess, time
from urllib.request import urlopen, Request
from tempfile import NamedTemporaryFile
import requests
VERSION = "0.1.2-cloudflare"
DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"
DEFAULT_STAGING_DIRECTORY_URL = "https://acme-staging-v02.api.letsencrypt.org/directory"
ACCOUNT_KEY_PATH = "/tmp/letsencrypt.key"
CF_API = "https://api.cloudflare.com/client/v4"
CF_TIMEOUT = 30
# record ids created during this run, keyed by (record name, record value)
CF_RECORD_IDS = {}
# zone ids resolved during this run, keyed by record name
CF_ZONE_CACHE = {}
def _cf_request(method, path, kwargs, params=None, payload=None):
token = kwargs.get('cf_api_token', None)
if not token:
raise Exception("Script param cf_api_token is required.")
headers = {"Authorization": "Bearer {}".format(token.strip()), "Content-Type": "application/json"}
r = requests.request(method, CF_API + path, headers=headers, params=params, json=payload, timeout=CF_TIMEOUT)
try:
body = r.json()
except ValueError:
raise Exception("Cloudflare API returned non-JSON response (HTTP {}): {}".format(r.status_code, r.text[:300]))
return r.status_code, body
def _cf_errors(body):
return "; ".join("{} {}".format(e.get('code'), e.get('message')) for e in (body.get('errors') or [])) or str(body)
def _cf_zone_id(record_name, kwargs):
zone_id = kwargs.get('cf_zone_id', None)
if zone_id:
return zone_id.strip()
if record_name in CF_ZONE_CACHE:
return CF_ZONE_CACHE[record_name]
# walk up the name until Cloudflare knows a zone: a.b.domain.tld -> b.domain.tld -> domain.tld
labels = record_name.split(".")
for i in range(1, len(labels) - 1):
candidate = ".".join(labels[i:])
code, body = _cf_request("GET", "/zones", kwargs, params={"name": candidate})
if code == 200 and body.get('success') and body.get('result'):
CF_ZONE_CACHE[record_name] = body['result'][0]['id']
return CF_ZONE_CACHE[record_name]
if code in (401, 403):
raise Exception("Cloudflare rejected the API token during zone lookup ({}). "
"Set cf_zone_id or add Zone / Zone / Read to the token.".format(_cf_errors(body)))
raise Exception("Could not find a Cloudflare zone for {}. Set the cf_zone_id script param.".format(record_name))
def _cf_find_record_ids(zone_id, record_name, value, kwargs):
code, body = _cf_request("GET", "/zones/{}/dns_records".format(zone_id), kwargs,
params={"type": "TXT", "name": record_name, "per_page": 100})
if code != 200 or not body.get('success'):
raise Exception("Cloudflare record lookup failed: {}".format(_cf_errors(body)))
# Cloudflare may return TXT content wrapped in double quotes
return [rec['id'] for rec in body.get('result', []) if rec.get('content', '').strip('"') == value]
def add_dns_text_record(key_digest_64, txt_record_name, kwargs):
# Create dns txt record with fqdn txt_record_name and value key_digest_64
name = txt_record_name.rstrip(".")
try:
ttl = max(60, int(kwargs.get('cf_ttl', 60)))
except ValueError:
ttl = 60
zone_id = _cf_zone_id(name, kwargs)
payload = {"type": "TXT", "name": name, "content": key_digest_64, "ttl": ttl}
code, body = _cf_request("POST", "/zones/{}/dns_records".format(zone_id), kwargs, payload=payload)
if code in (200, 201) and body.get('success'):
CF_RECORD_IDS[(name, key_digest_64)] = body['result']['id']
print("Added DNS TXT record {}".format(name))
return
# 81057 / 81058: an identical record is already there, e.g. left over from an aborted run
if any(e.get('code') in (81057, 81058) for e in (body.get('errors') or [])):
print("DNS TXT record {} already exists, reusing it".format(name))
return
raise Exception("Error adding DNS TXT record {} (HTTP {}): {}".format(name, code, _cf_errors(body)))
def remove_dns_text_record(key_digest_64, txt_record_name, kwargs):
# Remove the dns txt record with fqdn txt_record_name and value key_digest_64.
# Never raises: a cleanup problem must not hide the real result of the challenge.
name = txt_record_name.rstrip(".")
try:
zone_id = _cf_zone_id(name, kwargs)
record_id = CF_RECORD_IDS.pop((name, key_digest_64), None)
record_ids = [record_id] if record_id else _cf_find_record_ids(zone_id, name, key_digest_64, kwargs)
for rid in record_ids:
code, body = _cf_request("DELETE", "/zones/{}/dns_records/{}".format(zone_id, rid), kwargs)
if code == 200 and body.get('success'):
print("Deleted DNS TXT record {}".format(name))
else:
print("Warning: could not delete DNS TXT record {} ({}): {}".format(name, rid, _cf_errors(body)))
except Exception as e:
print("Warning: cleanup of DNS TXT record {} failed: {}".format(name, e))
def get_crt(csr, directory_url=DEFAULT_DIRECTORY_URL, contact=None, debug=False, kwargs=None):
directory, acct_headers, alg, jwk = None, None, None, None # global variables
# helper functions - base64 encode for jose spec
def _b64(b):
return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")
# helper function - run external commands
def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"):
proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate(cmd_input)
if proc.returncode != 0:
raise IOError("{0}\n{1}".format(err_msg, err))
return out
# helper function - make request and automatically parse json response
def _do_request(url, data=None, err_msg="Error", depth=0):
try:
ctx = ssl.create_default_context()
resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"}), context=ctx)
resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers
except IOError as e:
resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e)
code, headers = getattr(e, "code", None), {}
try:
resp_data = json.loads(resp_data) # try to parse json results
except ValueError:
pass # ignore json parsing errors
if depth < 100 and code == 400 and isinstance(resp_data, dict) and resp_data.get('type') == "urn:ietf:params:acme:error:badNonce":
raise IndexError(resp_data) # allow 100 retrys for bad nonces
if code not in [200, 201, 204]:
raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data))
return resp_data, code, headers
# helper function - make signed requests
def _send_signed_request(url, payload, err_msg, depth=0):
payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8'))
new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce']
protected = {"url": url, "alg": alg, "nonce": new_nonce}
protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']})
protected64 = _b64(json.dumps(protected).encode('utf8'))
protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8')
out = _cmd(["openssl", "dgst", "-sha256", "-sign", ACCOUNT_KEY_PATH], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error")
data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)})
try:
return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth)
except IndexError: # retry bad nonces (they raise IndexError)
return _send_signed_request(url, payload, err_msg, depth=(depth + 1))
# helper function - poll until complete
def _poll_until_not(url, pending_statuses, err_msg):
result, t0 = None, time.time()
while result is None or result['status'] in pending_statuses:
assert (time.time() - t0 < 600), "Polling timeout" # 10 minute timeout
time.sleep(0 if result is None else 2)
result, _, _ = _send_signed_request(url, None, err_msg)
return result
try:
propagation_wait = max(0, int(kwargs.get('cf_propagation_wait', 30)))
except ValueError:
propagation_wait = 30
if os.path.exists(ACCOUNT_KEY_PATH):
if debug:
print ("DEBUG: Reusing account key.")
else:
print ("Account key not found. Generating account key...")
out = _cmd(["openssl", "genrsa", "4096"], err_msg="OpenSSL Error")
with open(ACCOUNT_KEY_PATH, 'w') as f:
f.write(out.decode("utf-8"))
os.chmod(ACCOUNT_KEY_PATH, 0o600)
# parse account key to get public key
print ("Parsing account key...")
out = _cmd(["openssl", "rsa", "-in", ACCOUNT_KEY_PATH, "-noout", "-text"], err_msg="OpenSSL Error")
pub_pattern = r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)"
pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
alg = "RS256"
jwk = {
"e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"kty": "RSA",
"n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
}
accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
# find domains
print ("Parsing CSR...")
out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr))
domains = set([])
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"):
domains.add(san[4:])
if not domains:
raise ValueError("No domain names found in the CSR.")
print ("Found domains: {0}".format(", ".join(sorted(domains))))
# get the ACME directory of urls
print ("Getting directory...")
directory, _, _ = _do_request(directory_url, err_msg="Error getting directory")
print ("Directory found!")
# create account, update contact details (if any), and set the global key identifier
print ("Registering account...")
reg_payload = {"termsOfServiceAgreed": True}
account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering")
print ("Registered!" if code == 201 else "Already registered!")
if contact is not None:
account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details")
# Let's Encrypt no longer stores or returns contact addresses, so the key may be absent
returned = account.get('contact') if isinstance(account, dict) else None
print ("Contact details sent. Server returned: {0}".format(", ".join(returned) if returned else "none (not stored by this CA)"))
# create a new order
print ("Creating new order...")
order_payload = {"identifiers": [{"type": "dns", "value": d} for d in sorted(domains)]}
order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order")
print ("Order created!")
# Complete the authorizations. *.domain.tld and domain.tld both validate through
# _acme-challenge.domain.tld, each with its own TXT value. All TXT records are
# published BEFORE any challenge is triggered: Let's Encrypt's resolvers cache the
# TXT answer for a short time, so adding the second value after the first validation
# makes the second one fail against the cached (stale) answer.
pending = []
try:
for auth_url in order['authorizations']:
if debug:
print ("DEBUG: Authorization URL is: {}".format(auth_url))
authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges")
domain = authorization['identifier']['value']
label = "*.{}".format(domain) if authorization.get('wildcard') else domain
if authorization['status'] == "valid":
print ("{0} is already authorized, skipping challenge.".format(label))
continue
# find the dns-01 challenge and generate the txt record name and value
challenges = [c for c in authorization['challenges'] if c['type'] == "dns-01"]
if not challenges:
raise ValueError("No dns-01 challenge offered for {0}: {1}".format(label, authorization))
challenge = challenges[0]
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
keyauthorization = "{0}.{1}".format(token, thumbprint)
keydigest64 = _b64(hashlib.sha256(keyauthorization.encode("utf8")).digest())
txt_record_name = "_acme-challenge.{0}.".format(domain)
if debug:
print ("DEBUG: Validation record for {} is: {}".format(label, txt_record_name))
# register for cleanup before adding, so a half-created record is still removed
pending.append((label, auth_url, challenge, keydigest64, txt_record_name))
add_dns_text_record(keydigest64, txt_record_name, kwargs)
if pending:
print ("Waiting {} seconds for DNS propagation...".format(propagation_wait))
time.sleep(propagation_wait)
for label, auth_url, challenge, keydigest64, txt_record_name in pending:
print ("Verifying {0}...".format(label))
_send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(label))
authorization = _poll_until_not(auth_url, ["pending", "processing"], "Error checking challenge status for {0}".format(label))
if authorization['status'] != "valid":
raise ValueError("Challenge did not pass for {0}: {1}".format(label, authorization))
print ("Challenge passed for {0}".format(label))
finally:
if pending:
print ("Cleaning up...")
for label, auth_url, challenge, keydigest64, txt_record_name in pending:
remove_dns_text_record(keydigest64, txt_record_name, kwargs)
print ("All domains verified!")
# finalize the order with the csr
print ("Signing certificate...")
csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error")
_send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order")
# poll the order to monitor when it's done
order = _poll_until_not(order_headers['Location'], ["pending", "processing", "ready"], "Error checking order status")
if order['status'] != "valid":
raise ValueError("Order failed: {0}".format(order))
# download the certificate
certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed")
print ("Certificate signed!")
return certificate_pem
def certificate_request(csr, common_name, kwargs):
dry_run = kwargs.get('dryrun', "false")
contact = kwargs.get('contact', None)
debug = kwargs.get('debug', "false")
directory_url = kwargs.get('directory_url', None)
letsencrypt_key = kwargs.get('letsencrypt_key', None)
print ("Running version {}".format(VERSION))
debug = str(debug).lower() == "true"
if debug:
print ("Debug enabled.")
dry_run = str(dry_run).lower() == "true"
print ("dry_run is: {}".format(str(dry_run)))
if not directory_url:
directory_url = DEFAULT_STAGING_DIRECTORY_URL if dry_run else DEFAULT_DIRECTORY_URL
print ("directory_url is {}".format(directory_url))
if contact and "@" in contact:
contact = [ "mailto:{}".format(contact) ] # contact must be array as of ACME RFC
print ("Contact set to: {}".format(contact))
else:
contact = None
if letsencrypt_key:
with open(ACCOUNT_KEY_PATH, 'w') as f:
f.write(letsencrypt_key)
os.chmod(ACCOUNT_KEY_PATH, 0o600)
# Create CSR temp file.
csr_temp_file = NamedTemporaryFile(mode='w', delete=False)
csr_temp_file.close()
with open(csr_temp_file.name, 'w') as f:
f.write(csr)
signed_crt = None
try:
signed_crt = get_crt(csr_temp_file.name, directory_url=directory_url,
contact=contact, debug=debug, kwargs=kwargs)
finally:
os.remove(csr_temp_file.name)
print (signed_crt)
return signed_crt