HOW TO: Enable Kerberos Delegation For Flask On Linux
Tags: kerberos, s4u2proxy, linux, flask
Published: 2020-02-24 11:25:20

I recently encountered a scenario where I needed a Flask app running on Linux to pull data from a Microsoft SQL server running on Windows using the credentials of the caller and I was able to achieve this in a test environment using the below steps.

# Sections:
1. [Environment](#Environment)
1. [Steps](#Steps)
1. [Verification](#Verification)
1. [FAQ](#FAQ)

<a name="Environment"></a>

# Environment:

1. Windows 2016 server
    - Name: DC
    - Purpose: Active Directory Server
    - Domain: SSO
    - Services: Active Directory, DHCP, DNS
1. Windows 2016 server
    - Name: IIS1
    - Purpose: Hosts a website that will be called by Flaska
    - Domain: SSO
    - Services: IIS
1. Windows 2016 server
    - Name: MSSQL1
    - Purpose: Contains data that will be retrieved by Flask
    - Domain: SSO
    - Services: Microsoft SQL Server 2017
1. Centos 7
    - Name: flask.sso.local
    - Purpose: Host Flask app
    - Domain: **\<NOT JOINED TO A DOMAIN\>**
    - Services: httpd

<a name="Steps"></a>

# Steps:
1. Create a new user account in active directory (E.x: **flask_svc_acct**)

2. Generate the keytab file on the Active Director server (**NOTE**: *sa* is the password I assigned to the account **flask_svc_acct**)
    - *ktpass.exe -princ HTTP/flask.sso.local@SSO.LOCAL -mapuser flask_svc_acct@SSO.LOCAL -pass sa -crypto ALL -ptype KRB5_NT_PRINCIPAL -out C:\Temp\http_flask_svc_acct.keytab*

3. Grant the account **flask_svc_acct** unconstrained delegation
    - Enable **Trust this user for delegation to any service (Kerberos only)** in the *Delegation* tab for the user **flask_svc_acct** in Active Directory.

4. Copy the keytab file generated in the previous step to the linux box. (E.x: **/etc**)
    - I recommend using [MobaXterm](https://mobaxterm.mobatek.net). It lets you copy files from windows, ssh into linux boxes and has a GUI for editing text files on the linux box.

5. Install the packages needed to use python, kerberos and apache
    - yum install -y yum-utils
    - yum install -y https://centos7.iuscommunity.org/ius-release.rpm
    - yum groupinstall -y "Development Tools"
    - yum install -y gcc gcc-c++ krb5-workstation krb5-libs krb5-auth-dialog krb5-devel httpd mod_auth_kerb unixODBC-devel openssl-devel bzip2-devel libffi-devel wget httpd-devel

6. Installing the Microsoft ODBC Driver for SQL Server (**NOTE: You can skip this step if you don't intend to connect to Microsoft SQL Server**)
    - curl https://packages.microsoft.com/config/rhel/7/prod.repo > /etc/yum.repos.d/mssql-release.repo
    - yum remove unixODBC-utf16 unixODBC-utf16-devel #to avoid conflicts
    - ACCEPT_EULA=Y yum install -y msodbcsql17

7. Grant apache read access to the keytab file
    - chown apache /etc/http_flask_svc_acct.keytab
    - chmod 400 /etc/http_flask_svc_acct.keytab

8. Install Python 3.8.1
    - cd /tmp
    - wget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz
    - tar xvf Python-3.8.1.tgz
    - cd Python-3.8.1
    - ./configure --enable-ipv6 --with-system-expat --with-system-ffi --enable-loadable-sqlite-extensions --enable-optimizations --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
    - make altinstall

9. Compile mod_wsgi
    - cd /tmp
    - python3.8 -m venv mod_wsgi
    - source mod_wsgi/bin/activate
    - pip3.8 install mod_wsgi
    - deactivate
 
10. Copy the compiled mod_wsgi to apache's module directory
    - cp mod_wsgi/lib/python3.8/site-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so /etc/httpd/modules/mod_wsgi.so

11. Create the file /etc/httpd/conf.modules.d/10-wsgi.conf with the content **LoadModule wsgi_module modules/mod_wsgi.so**
	- echo 'LoadModule wsgi_module modules/mod_wsgi.so' > /etc/httpd/conf.modules.d/10-wsgi.conf
12. Update the firewall to allow Apache to receive traffic
    - firewall-cmd --permanent --add-service=http
    - firewall-cmd --reload

16. Disable Security-Enhanced Linux
    - setenforce 0

17. Make /etc/krb5.conf look like
    ```
	# Configuration snippets may be placed in this directory as well
	includedir /etc/krb5.conf.d/
	
	[logging]
	 default = FILE:/var/log/krb5libs.log
	 kdc = FILE:/var/log/krb5kdc.log
	 admin_server = FILE:/var/log/kadmind.log
	
	[libdefaults]
	 dns_lookup_realm = false
	 ticket_lifetime = 24h
	 renew_lifetime = 7d
	 forwardable = true
	 rdns = false
	 pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
	 default_realm = SSO.LOCAL
	 default_ccache_name = KEYRING:persistent:%{uid}
	
	[realms]
	 SSO.LOCAL = {
	 default_domain = sso.local
	 }
	
	[domain_realm]
	 .sso.local = SSO.LOCAL
	 sso.local = SSO.LOCAL
    ```
19. Save the below content to **/etc/httpd/conf.d/auth_kerb.conf**
	```
	#http://modauthkerb.sourceforge.net/configure.html
	<Location />
		  AuthType Kerberos
		  AuthName "Kerberos Login"
		  Krb5KeyTab /etc/http_flask_svc_acct.keytab
		  require valid-user
		  KrbSaveCredentials on
		  KrbConstrainedDelegation on
		  KrbMethodNegotiate on
	</Location>
	```

21. Change the value of *LogLevel* in **/etc/httpd/conf/httpd.conf** to *trace8*

13. Create a subdirectory in /var/www to store and run our flask app (E.x.: **/var/www/kerberos_test**)
    - mkdir /var/www/kerberos_test

14. Go to the directory created in the prior step and create a python virtual environment for the flask app. I'll be using the virtual environment name **venv**
    - cd /var/www/kerberos_test
    - python3.8 -m venv venv

15. Install the modules **flask requests requests-kerberos pyodbc** in the virtual environment
    - source venv/bin/activate
    - pip3.8 install flask requests requests-kerberos pyodbc
    - deactivate

18. Create the flask app (E.x.: **/var/www/kerberos_test/wsgi.py**)
	```
	import flask
	import requests
	import os
	import pyodbc
	import sys
	import gc
	from requests_kerberos import HTTPKerberosAuth
	from contextlib import ExitStack
	
	application = flask.Flask(__name__)
	
	KRB5_THREADED_KEYRING_CCNAME = "KEYRING:thread:"
	
	@application.before_first_request
	def before_fist_req():
	  os.environ["KRB5CCNAME"] = KRB5_THREADED_KEYRING_CCNAME
	
	@application.before_request
	def before_req():
	  krb5_cc_copy(flask.request.environ["KRB5CCNAME"], KRB5_THREADED_KEYRING_CCNAME)
	
	@application.route('/')
	def hello_world():
	  with ExitStack() as disposables:
	    dest = "http://iis1.sso.local/wai.aspx"
	    r = disposables.enter_context(requests.get(dest, auth=HTTPKerberosAuth(delegate=True)))
	    conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=mssql1.sso.local;Trusted_Connection=yes;"
	    conn = disposables.enter_context(pyodbc.connect(conn_str))
	    cursor = conn.cursor()
	    query = "SELECT SYSTEM_USER, * FROM sys.dm_exec_sessions"
	    cursor.execute(query)
	    row = cursor.fetchone()
	
	    return f"""
		<br>
		{sys.version}
		{flask.request.environ}
		<br>
		{dest}
		<br>
		headers:{r.headers}
		<br>
		text:{r.text}
		<br>
		{query}:{row}
		"""
	
	# Below code should be moved to separate module (E.x.: libkrb5_helper.py )
	
	from contextlib import ExitStack
	from ctypes import CDLL, byref, c_char_p, c_void_p, cast
	libkrb5 = CDLL("libkrb5.so")
	
	libkrb5.krb5_get_error_message.restype = c_void_p
	libkrb5.krb5_free_error_message.argtypes = (c_void_p, c_void_p)
	
	def krb5_raise_error(krb5_error_code, func, arguments):
	  context = arguments[0]
	  if krb5_error_code:    
	    err_msg_ptr = libkrb5.krb5_get_error_message(context, krb5_error_code)
	    try:
	      err_msg = cast(err_msg_ptr, c_char_p).value.decode()
	      raise Exception(err_msg)
	    finally:
	      libkrb5.krb5_free_error_message(context, err_msg_ptr)
	
	libkrb5.krb5_cc_resolve.errcheck = krb5_raise_error
	libkrb5.krb5_cc_get_principal.errcheck = krb5_raise_error
	libkrb5.krb5_cc_initialize.errcheck = krb5_raise_error
	libkrb5.krb5_cc_copy_creds.errcheck = krb5_raise_error
	libkrb5.krb5_cc_destroy.errcheck = krb5_raise_error
	libkrb5.krb5_cc_close.errcheck = krb5_raise_error
	  
	def krb5_cc_copy(src_cc_name, dest_cc_name):
	  thread_ccache = c_void_p()
	  src_cc = c_void_p()
	  principal = c_void_p()
	  krb5_context = c_void_p()
	  with ExitStack() as krb_cleanup:    
	    krb5_error_code = libkrb5.krb5_init_context(byref(krb5_context))
	    if krb5_error_code:
	      raise Exception(f"Call to krb5_init_context failed with error code [{krb5_error_code}].")
	    krb_cleanup.callback(libkrb5.krb5_free_context, krb5_context)      
	    
	    libkrb5.krb5_cc_resolve(krb5_context, dest_cc_name.encode(), byref(thread_ccache))
	    krb_cleanup.callback(libkrb5.krb5_cc_close, krb5_context, thread_ccache)
	    
	    libkrb5.krb5_cc_resolve(krb5_context, src_cc_name.encode(), byref(src_cc))
	    krb_cleanup.callback(libkrb5.krb5_cc_close, krb5_context, src_cc)
	    
	    libkrb5.krb5_cc_get_principal(krb5_context, src_cc, byref(principal))
	    krb_cleanup.callback(libkrb5.krb5_free_principal, krb5_context, principal)
	    
	    libkrb5.krb5_cc_initialize(krb5_context, thread_ccache, principal)
	    libkrb5.krb5_cc_copy_creds(krb5_context, src_cc, thread_ccache)
	
	```

20. Add the following entry to **/etc/httpd/conf/httpd.conf**

	```
	<VirtualHost *:80>
 WSGIDaemonProcess kerberos_test python-home=/var/www/kerberos_test/venv
 WSGIScriptAlias /kerberos_test /var/www/kerberos_test/wsgi.py
 <Directory /var/www/kerberos_test>
     Require valid-user
 </Directory>
 <Location /kerberos_test>
   WSGIProcessGroup kerberos_test
 </Location>
</VirtualHost>
	```	

22. Restart Apache
    - systemctl restart httpd

23. Tail the log file
    - tail -f /etc/httpd/logs/error_log

24. Change Apache's LogLevel back to warn once you've verified everything is working

<a name="Verification"></a>

# Verification:
You can verify Kerberos authentication works by running the following code in [LINQPad](https://www.linqpad.net)
```csharp
var wc = new System.Net.WebClient{UseDefaultCredentials = true};
wc.DownloadString("http://flask.sso.local/kerberos_test/").Dump();
wc.UseDefaultCredentials = false;
wc.DownloadString("http://flask.sso.local/kerberos_test/").Dump();
```
And the output should look similar to below.
I ran the c# code under the user account **SSO\bob** so you'll see that the flask server was able to connect to both IIS and Microsoft SQL Server as **SSO\bob** when I enabled *UseDefaultCredentials* and the flask server returned status code 401 when I disabled *UseDefaultCredentials* because I configured Apache to only allow domain users.
```

  <br>
  3.8.1 (default, Mar 16 2020, 14:43:00) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
  {'UNIQUE_ID': 'AAAAAKVh3US3dOPcBnV2lAAAAAg', 'KRB5CCNAME': 'FILE:/run/httpd/krbcache/krb5cc_apache_tiJTTA', 'GATEWAY_INTERFACE': 'CGI/1.1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'REQUEST_URI': '/kerberos_test/', 'SCRIPT_NAME': '/kerberos_test', 'PATH_INFO': '/', 'PATH_TRANSLATED': '/var/www/html/', 'HTTP_HOST': 'flask.sso.local', 'SERVER_SIGNATURE': '', 'SERVER_SOFTWARE': 'Apache/2.4.6 (CentOS) mod_auth_kerb/5.4 mod_wsgi/4.7.1 Python/3.8', 'SERVER_NAME': 'flask.sso.local', 'SERVER_ADDR': '10.0.0.8', 'SERVER_PORT': '80', 'REMOTE_ADDR': '10.0.0.1', 'DOCUMENT_ROOT': '/var/www/html', 'REQUEST_SCHEME': 'http', 'CONTEXT_PREFIX': '', 'CONTEXT_DOCUMENT_ROOT': '/var/www/html', 'SERVER_ADMIN': 'root@localhost', 'SCRIPT_FILENAME': '/var/www/kerberos_test/wsgi.py', 'REMOTE_PORT': '57887', 'REMOTE_USER': 'bob@SSO.LOCAL', 'AUTH_TYPE': 'Negotiate', 'mod_wsgi.script_name': '/kerberos_test', 'mod_wsgi.path_info': '/', 'mod_wsgi.process_group': 'kerberos_test', 'mod_wsgi.application_group': 'flask.sso.local|/kerberos_test', 'mod_wsgi.callable_object': 'application', 'mod_wsgi.request_handler': 'wsgi-script', 'mod_wsgi.handler_script': '', 'mod_wsgi.script_reloading': '1', 'mod_wsgi.listener_host': '', 'mod_wsgi.listener_port': '80', 'mod_wsgi.enable_sendfile': '0', 'mod_wsgi.ignore_activity': '0', 'mod_wsgi.request_start': '1584910828940831', 'mod_wsgi.request_id': 'AAAAAKVh3US3dOPcBnV2lAAAAAg', 'mod_wsgi.connection_id': 'Dq71zrcrMRg', 'mod_wsgi.queue_start': '1584910828942817', 'mod_wsgi.daemon_connects': '1', 'mod_wsgi.daemon_restarts': '0', 'mod_wsgi.daemon_start': '1584910828942951', 'mod_wsgi.script_start': '1584910828943034', 'wsgi.version': (1, 0), 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.errors': <_io.TextIOWrapper name='<wsgi.errors>' encoding='utf-8'>, 'wsgi.input': <mod_wsgi.Input object at 0x7fcaa44d6ce0>, 'wsgi.input_terminated': True, 'wsgi.file_wrapper': <class 'mod_wsgi.FileWrapper'>, 'apache.version': (2, 4, 6), 'mod_wsgi.version': (4, 7, 1), 'mod_wsgi.total_requests': 18122, 'mod_wsgi.thread_id': 1, 'mod_wsgi.thread_requests': 9061, 'werkzeug.request': <Request 'http://flask.sso.local/kerberos_test/' [GET]>}
  <br>
  http://iis1.sso.local/wai.aspx
  <br>
  headers:{'Cache-Control': 'private', 'Content-Type': 'text/html; charset=utf-8', 'Content-Encoding': 'gzip', 'Vary': 'Accept-Encoding', 'Server': 'Microsoft-IIS/10.0', 'X-AspNet-Version': '4.0.30319', 'Persistent-Auth': 'true', 'X-Powered-By': 'ASP.NET', 'WWW-Authenticate': 'Negotiate YIGYBgkqhkiG9xIBAgICAG+BiDCBhaADAgEFoQMCAQ+ieTB3oAMCARKicARuRutiYUwRhiQtpfcCsipTRT70KVovTi/JbGwACalJeFqAaLvcd4zI0aPjr+nkVvAtbNLnCr7+umNmxxXjEeRBxtjv4S2/lXXw41zVfnDjkxZmxPSAgGFcYMN7mjnzomgNjTBlVqPR7xD2KifyElI=', 'Date': 'Sun, 22 Mar 2020 21:00:29 GMT', 'Content-Length': '199'}
  <br>
  text:
<!DOCTYPE html>
<html>
<body>

<h1>
HELLO FROM IIS
<br>
SSO\bob</h1>

<p>
SSO\bob</p>

</body>
</html>

  <br>
  SELECT SYSTEM_USER, * FROM sys.dm_exec_sessions:('SSO\\bob', 52, datetime.datetime(2020, 3, 22, 17, 0, 29, 617000), 'flask.sso.local', 'httpd', 1140, 7, 'ODBC', b'\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xb1$v\xb9\x047\xee\x1aNf\x97\xf5S\x04\x00\x00', 'SSO\\bob', 'SSO', 'bob', 'running', b'', 0, 3, 0, 0, 4, datetime.datetime(2020, 3, 22, 17, 0, 29, 617000), datetime.datetime(2020, 3, 22, 17, 0, 29, 613000), 0, 0, 0, True, -1, 'us_english', 'mdy', 7, True, False, True, False, True, True, True, True, 2, -1, 0, 0, 0, b'\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xb1$v\xb9\x047\xee\x1aNf\x97\xf5S\x04\x00\x00', 'SSO\\bob', None, None, None, 1, 1, 1, 1)
  


6WebException4 
The remote server returned an error: (401) Unauthorized. 
```

<a name="FAQ"></a>

# FAQ:
1. Does the linux machine need to join the domain?
    - No

1. Why do you disable Security-Enhanced Linux (SELinux)?
    - I was getting the error  
*ImportError: /var/www/kerberos_test/venv/lib/python3.8/site-packages/pyodbc.cpython-38-x86_64-linux-gnu.so: failed to map segment from shared object: Permission denied*  
in the log files when it was enabled. I found the work around in [Centos 6.4 - Failed to map segment from shared object: Permission denied
](https://stackoverflow.com/questions/20919771/centos-6-4-failed-to-map-segment-from-shared-object-permission-denied).

1. How do I disable SELinux permanently?
    - set **SELINUX** to ***permissive*** in **/etc/selinux/config**

1. Do I have to use unconstrained delegation to make this work?
    - No. It's recommended that you use constrained delegation

1. I get the error **[auth_kerb:error] [pid 1562] [client 10.0.0.1:54363] gss_acquire_cred() failed: Unspecified GSS failure.  Minor code may provide more information (, No key table entry found matching HTTP/flask.sso.local@)**
    - This can be for a number of reasons. Make sure the hostname is all lower case and contains the domain name. (E.x.: The hostname in my example must be flask.sso.local instead of flask, Flask, Flask.sso.local or FLASK.sso.local.
    - Verify the kerberos settings are correct by running **kinit** on the keytab file (E.x.: kinit -vkt /etc/http_flask_svc_acct.keytab) You should get something like **kinit: Keytab contains no suitable keys for host/flask.sso.local@SSO.LOCAL while getting initial credentials**.
    - Use **klist** to see the entries in the keytab file (E.x.: klist -kte /etc/http_flask_svc_acct.keytab) and make sure it has the expected encryption type and principal).

1. I get the error **pyodbc.Error: ('HY000', "[HY000] [Microsoft][ODBC Driver 17 for SQL Server]SSPI Provider: KDC can't fulfill requested option (851968) (SQLDriverConnect)")**
    - Try having the client connect with the username in all lowercase. (E.x.: If the user name is **Bob**, try logging in as **bob**)

1. How do I use **AES256-SHA1** encryption for keytabs?
    - Enable **This account supports Kerberos AES 256 bit encyrption** on the service account and every account that will access the website in the `Account options` in Active Directory

1. How do I have Apache start automatically after a reboot?
    - systemctl enable httpd

1. How do I host multiple flask apps?
    - Make your entry for the *VirtualHost* in **/etc/httpd/conf/httpd.conf** look similar to the one below
    ````
    <VirtualHost *:80>
     WSGIDaemonProcess kerberos_test python-home=/var/www/kerberos_test/venv
     WSGIScriptAlias /kerberos_test /var/www/kerberos_test/wsgi.py
     <Directory /var/www/kerberos_test>
         Require valid-user
     </Directory>
     <Location /kerberos_test>
       WSGIProcessGroup kerberos_test
     </Location>
     
     WSGIDaemonProcess some_other_flask_app python-home=/var/www/some_other_flask_app/venv
     WSGIScriptAlias /some_other_flask_app /var/www/some_other_flask_app/wsgi.py
     <Directory /var/www/some_other_flask_app>
         Require valid-user
     </Directory>
     <Location /some_other_flask_app>
       WSGIProcessGroup some_other_flask_app
     </Location>
    </VirtualHost>
    ````