Certbot running through Flask, Python subprocess.Popen returns early


#1

Please fill out the fields below so we can help you better. Note: you must provide your domain name to get help. Domain names for issued certificates are all made public in Certificate Transparency logs (e.g. https://crt.sh/?q=example.com), so withholding your domain name here does not increase secrecy, but only makes it harder for us to provide help.

My domain is:makemysite.org

I ran this command: certbot certonly --test-cert --cert-name makemysite --webroot-path /var/www/makemysite -d makemysite.org

It produced this output: returned to Apache, never returned to my Python code

My web server is (include version): Apache V 2

The operating system my web server runs on is (include version): Ubuntu 16.04.4

My hosting provider, if applicable, is: me

I can login to a root shell on my machine (yes or no, or I don’t know): yes

I’m using a control panel to manage my site (no, or provide the name and version of the control panel): No control Panel


I’m having a very strange problem with certbot obtaining a certificate when running in a Flask server under Apache (on Ubuntu 16.04). I’m using pyCharm and have setup remote debugging to the server. I have to use the embedded HTTP server that is part of Flask for the remote debugger to work. Certbot works just fine in my debugging environment, yet is fails running under Apache.

The problem is: When I attempt to get a cert with, certbot certonly --test-cert --cert-name makemysite --webroot-path /var/www/makemysite -d makemysite.org certbot returns almost immediately through Apache to the caller with an undefined error. Yet, as I watch the letsencrypt log file I can see a certificate is being issued after Apache returned. In the end I do get my certificate, yet since I no longer return from the Python subprocess call I do not finish executing the code required to manage the certificate.

I have wrapped my Python code in try/except/finally constructs to log the return path. The sub-process call does indeed return when running in the debugger, but it does not return when running under Apache.

I have tried to use the --preferred-challenges http option, but it only works with the Standalone server. This failed as I really need to run under Apache, and I cannot kill Apache as I am serving multiple web sites.

As I reviewed by work, one more time, I noticed the over sized response in the Apache error log. I update the sudoCommand method below to use the tempfile.SpoolTemporaryFile for both stdout and stderr. This did not solve the problem.

Of course any help to resolve this problem would be appreciated.

Thanks in advance,

Jeffrey

From the Apache error log:
[Mon Nov 26 09:03:42.649325 2018] [mpm_prefork:notice] [pid 1274] AH00171: Graceful restart requested, doing restart
[Mon Nov 26 09:03:45.662507 2018] [wsgi:error] [pid 4220] [client 50.242.66.243:41460] Truncated or oversized response headers received from daemon process ‘chAdmin’: /var/www/chAdmin/chAdmin.wsgi
[Mon Nov 26 09:03:45.691876 2018] [ssl:warn] [pid 1274] AH01909: chAdmin:443:0 server certificate does NOT include an ID which matches the server name
[Mon Nov 26 09:03:45.692011 2018] [wsgi:warn] [pid 1274] mod_wsgi: Compiled for Python/3.5.1+.
[Mon Nov 26 09:03:45.692019 2018] [wsgi:warn] [pid 1274] mod_wsgi: Runtime using Python/3.5.2.
[Mon Nov 26 09:03:45.692631 2018] [mpm_prefork:notice] [pid 1274] AH00163: Apache/2.4.18 (Ubuntu) OpenSSL/1.0.2g mod_wsgi/4.3.0 Python/3.5.2 configured – resuming normal operations
[Mon Nov 26 09:03:45.692644 2018] [core:notice] [pid 1274] AH00094: Command line: ‘/usr/sbin/apache2’

“”"
getAndInstallLeSslCerts
“”"
def getAndInstallLeSslCerts(self, certName, nativeUrl, realCert):
frameInfo = getframeinfo(currentframe())
fileName = os.path.basename(frameInfo.filename)
functName = frameInfo.function
if self.debug > 2:
logging.info("… {0}:{1}".format(fileName, functName))
try:
# Issue the certbot command
if realCert == ‘True’:
cmdLine = (
“sudo -S -H certbot certonly”
" --cert-name {0}"
" --webroot-path /var/www/{1}"
" -d {2}".format(certName, certName, nativeUrl))
else:
cmdLine = (
“sudo -S -H certbot certonly "
" --test-cert”
" --cert-name {0}"
" --webroot-path /var/www/{1}"
" -d {2}".format(certName, certName, nativeUrl))
retCode = self.utils.sudoCommands(cmdLine, response=3)
if retCode != 0:
outs = self.utils.outs
errs = self.utils.errs
logging.info (’\t command line: {0}’.format(cmdLine))
logging.info (’\t return code: {0}’.format(retCode))
logging.info (’\t stderr:’)
lines = errs.splitlines(keepends=True)
for line in lines:
logging.info (’\t\t{0}’.format(line))
logging.info (’\t stdout:’)
lines = outs.splitlines(keepends=True)
for line in lines:
logging.info (’\t\t{0}’.format(line))
raise ValueError(“LE SSL cert procurement failed”)
except Exception as ex:
template = “An exception of type {0} occurred. Arguments:\n\t{1!r}”
message = template.format(type(ex).name, ex.args)
logging.info ("{0}: ERROR: {1}".format(functName, message))
raise
finally:
if self.debug > 3:
msg = “Exiting getAndInstallLeSslCerts”
logging.info(msg)
return

“”"
sudoCommands
“”"
def sudoCommands(self, cmdLine, response=None):
frameInfo = getframeinfo(currentframe())
fileName = os.path.basename(frameInfo.filename)
functName = frameInfo.function
if chag.debug > 2:
logging.info("… {0}:{1}:{2}".format(fileName, functName, cmdLine))
try:
# Create a list of the command components.
cmdList = shlex.split(cmdLine)
# Execute the sudo command.
p = subprocess.Popen(cmdList, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
“”"
All sudo commands require a password. Some commands require a response.
If a response is required we simply build a string with a new line.
The entire sequence of multiple commands can get very complex and very messy. Just
think timeouts, best guesses, and lots of error recovery.
So for now, I’m just handling what we need.
“”"
if not response:
pw = “{0}\n”.format(self.sudoPw)
outs, errs = p.communicate(bytearray(pw, ‘utf-8’))
else:
input = “{0}\n{1}”.format(self.sudoPw, response)
outs, errs = p.communicate(bytearray(input, ‘utf-8’))
self._outs = outs
self._errs = errs
# Wait for the sub-process to finish
p.wait()
# Pickup the return code
retCode = p.returncode
“”"
Debugging
“”"
if self.__sudoDebug > 3:
logging.info (‘sudo debug’)
logging.info (’\t command line: {0}’.format(cmdLine))
logging.info (’\t return code: {0}’.format(retCode))
logging.info (’\t stderr:’)
lines = errs.splitlines(keepends=True)
for line in lines:
logging.info (’\t\t{0}’.format(line))
logging.info (’\t stdout:’)
lines = outs.splitlines(keepends=True)
for line in lines:
logging.info (’\t\t{0}’.format(line))
if retCode > 0:
msg = “sudo command FAILED: {0}”.format(cmdLine)
raise ValueError(msg)
except Exception as ex:
template = “An exception of type {0} occurred. Arguments:\n\t{1!r}”
message = template.format(type(ex).name, ex.args)
logging.info("{0}: ERROR: {1}".format(functName, message))
return retCode


#2

I don’t know what your use case for this is in the end, but trying to interface with Certbot like this seems like a more complicated and less reliable way of going about it. Rather, I’d recommend looking into a more “native” solution, such as using a Python ACME client. There are probably more implementations you could find, but I think acme-python would probably be a good place to start.


#3

Thanks,
I’ll look into the acme-python.

Jeffrey


#4

Not directly relevant, but building root string command lines is asking for trouble.

What if a user says that certName is example --pre-hook "install a backdoor"?


#5

That would be very bad news indeed.
Guess it is time to go to acme-python and decouple to a very protected daemon…