DEV Community

hjb417
hjb417

Posted on • Edited on

HOW TO: Enable Kerberos Delegation For Flask On Linux

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
  2. Steps
  3. Verification
  4. FAQ

Environment:

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

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)

  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. 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)

  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
  13. Disable Security-Enhanced Linux

    • setenforce 0
  14. 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
    
  15. 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>
    
  16. Change the value of LogLevel in /etc/httpd/conf/httpd.conf to trace8

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

    • mkdir /var/www/kerberos_test
  18. 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
  19. 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
  20. 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)
    
    
  21. 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

Verification:

You can verify Kerberos authentication works by running the following code in LINQPad

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. 

FAQ:

  1. Does the linux machine need to join the domain?

    • No
  2. Why do you disable Security-Enhanced Linux (SELinux)?

  3. How do I disable SELinux permanently?

    • set SELINUX to permissive in /etc/selinux/config
  4. Do I have to use unconstrained delegation to make this work?

    • No. It's recommended that you use constrained delegation
  5. 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).
  6. 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)
  7. 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
  8. How do I have Apache start automatically after a reboot?

    • systemctl enable httpd
  9. 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>
    


    `

Top comments (0)