I have a web server that maintains most of its content in a local database, but needs to query a back-end directory service to retrieve user information. The directory query works fine as a stand-alone operation, but when the web server makes the query, ssl returns an error.
The server is based on CentOS 7-2.1511 / Django 1.9.3 / PostgreSQL 9.2.15 / Apache 2.4.6-40 / mod_ssl 1:2.4.6-40 / OpenSSL 1:1.0.1 / Python 3.4.3
. Apache uses mod_ssl to serve https: requests from the client (browser), and I'm assuming Python's ssl.py uses the same engine to make https: requests to the directory server. Python SSL says that it's implementation is based on OpenSSL. Yum apparently can't list mod_ssl's dependencies, but I assume it also uses the installed version of openssl.
The following code will take the user's distinguished name (extracted from SSL_CLIENT_CERT) and query the directory server for the user's attributes using a RESTful interface:
import requests, urllib
URL = 'https://example.com/rest/user_info/'
def get_user_info(dn)
query = URL + urllib.parse.quote(dn)
return requests.get(query, cert=('server.crt', 'server.key'),
verify='ca_bundle.crt').json()
When I am running on the server as user apache in the server's WSGI directory the routine correctly returns a dict containing user attributes:
$ python
>>> import auth
>>> dn='cn=Me,o=Company,c=US'
>>> attr = auth.get_user_info(dn)
But when Apache calls the same function with the same DN from it's WSGI script (views.py
), it raises an OSError:
OSError(0, 'Error')
Line 810, /lib64/python3.4/ssl.py
803 def do_handshake(self, block=False):
804 """Perform a TLS/SSL handshake."""
805 self._check_connected()
806 timeout = self.gettimeout()
807 try:
808 if timeout == 0.0 and block:
809 self.settimeout(None)
810 self._sslobj.do_handshake()
I will start researching locking as suggested for OpenSSL (since I can't think of anything else that would be causing these errors) but it's hard to believe that a webserver using SSL for backend queries isn't already a well-traveled path. Questions:
I give up. The following works reliably, at the expense of both beauty and efficiency. It tries a requests
query first, and if that blows up, it punts and calls wget
in a subprocess. It returns a meta item $method
that lets the page view know whether the inline request failed.
def get_user_info(dn, sub=True, ttl=300):
query = URL + urllib.parse.quote(dn)
try:
info = requests.get(query, cert=(SERVER_CERT, SERVER_KEY),
verify=CA_CERT).json()
info['$method'] = 'requests'
return info
except OSError:
if sub:
args = ['wget', '-O', '-',
'--certificate=' + SERVER_CERT,
'--private-key=' + SERVER_KEY,
query]
bytes = subprocess.check_output(args, timeout=5)
info = json.loads(bytes.decode(encoding='utf-8'))
info['$method'] = 'subprocess'
return info
else:
raise
It sure would be nice if OpenSSL used contexts instead of global variables.