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:
Environment:
- Windows 2016 server
- Name: DC
- Purpose: Active Directory Server
- Domain: SSO
- Services: Active Directory, DHCP, DNS
- Windows 2016 server
- Name: IIS1
- Purpose: Hosts a website that will be called by Flaska
- Domain: SSO
- Services: IIS
- Windows 2016 server
- Name: MSSQL1
- Purpose: Contains data that will be retrieved by Flask
- Domain: SSO
- Services: Microsoft SQL Server 2017
- Centos 7
- Name: flask.sso.local
- Purpose: Host Flask app
- Domain: <NOT JOINED TO A DOMAIN>
- Services: httpd
Steps:
Create a new user account in active directory (E.x: flask_svc_acct)
-
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
-
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.
-
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.
-
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
-
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
-
Grant apache read access to the keytab file
- chown apache /etc/http_flask_svc_acct.keytab
- chmod 400 /etc/http_flask_svc_acct.keytab
-
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
-
Compile mod_wsgi
- cd /tmp
- python3.8 -m venv mod_wsgi
- source mod_wsgi/bin/activate
- pip3.8 install mod_wsgi
- deactivate
-
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
-
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
-
Update the firewall to allow Apache to receive traffic
- firewall-cmd --permanent --add-service=http
- firewall-cmd --reload
-
Disable Security-Enhanced Linux
- setenforce 0
-
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
-
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>
Change the value of LogLevel in /etc/httpd/conf/httpd.conf to trace8
-
Create a subdirectory in /var/www to store and run our flask app (E.x.: /var/www/kerberos_test)
- mkdir /var/www/kerberos_test
-
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
-
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
-
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)
-
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>
-
Restart Apache
- systemctl restart httpd
-
Tail the log file
- tail -f /etc/httpd/logs/error_log
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:
-
Does the linux machine need to join the domain?
- No
-
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 .
-
How do I disable SELinux permanently?
- set SELINUX to permissive in /etc/selinux/config
-
Do I have to use unconstrained delegation to make this work?
- No. It's recommended that you use constrained delegation
-
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).
-
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)
-
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
- Enable This account supports Kerberos AES 256 bit encyrption on the service account and every account that will access the website in the
-
How do I have Apache start automatically after a reboot?
- systemctl enable httpd
-
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)