Access Windows Active Directory Domain Services with Kerberos Authentication from OpenResty

Background

kerberos

Kerberos is a computer network security protocol that authenticates service requests between two or more trusted hosts across an untrusted network, like the internet. It uses secret-key cryptography and a trusted third party for authenticating client-server applications and verifying users’ identities.

kerberos-key

Since Windows 2000, Microsoft has used the Kerberos protocol as the default authentication method in Windows, and it is an integral part of the Windows Active Directory (AD) service.

LDAP is commonly used to do authentication and authorization.

Windows ADDS uses LDAP protocol, but normally the enterprise ADDS will use Kerberos as authentication method, instead of simple bind.

How to access ADDS/Kerberos from OpenResty/Nginx?

As known, OpenResty does not have a fully functional LDAP library.

Let’s have a look at the current alternatives:

  • lualdap
    • No SASL auth, simple bind only
    • Not based on cosocket, i.e. not async
  • lua-resty-ldap
    • No SASL auth, simple bind only

What about other programming lanuages?

After investigation, I think bonsai is the best choice, which is a popular and active python ldap client library.

Highlights:

  • asyncio support
  • Full SASL support
    • DIGEST-MD5 and NTLM
    • GSSAPI and GSS-SPNEGO (keytab, ad-hoc credential support)
    • EXTERNAL
  • simple pythonic design
  • based on robust and time-tested C libraries, e.g. libldap2, libsasl2, libkrb5

Why not encapsulate it so that we could reuse it in openresty?

lua-resty-ffi provides an efficient and generic API to do hybrid programming in openresty with mainstream languages (Go, Python, Java, Rust, Nodejs).

lua-resty-ffi-ldap = lua-resty-ffi + bonsai

Check this repo for source code:

https://github.com/kingluo/lua-resty-ffi-ldap

Demo

Set up a minimal ADDS

Let’s install a Windows ADDS on Window Server 2022 in VM.

From the server manager, add ADDS feature:

setup_adds_step1

Promote this server to a domain controller:

setup_adds_step2

setup_adds_step3

setup_adds_step4

setup_adds_step5

Add a user chuck:

setup_adds_step6

Add a group foobar:

setup_adds_step8

setup_adds_step7

Attach user chuck to group foobar:

setup_adds_step9

Done!

Use lua-resty-ffi-ldap

Add Windows IP into /etc/hosts:

# Hostname is windows
# Domain is bonsai.test
# IP is local network IP 172.30.90.18
172.30.90.18       windows.bonsai.test bonsai.test

Edit krb5.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[libdefaults]
    default_realm = BONSAI.TEST
    ccache_type = 3
    rdns = false

[realms]
    BONSAI.TEST = {
        kdc = windows.bonsai.test:88
        admin_server = windows.bonsai.test:749
    }

[domain_realm]
    .bonsai.test = BONSAI.TEST
    bonsai.test = BONSAI.TEST

demo.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
local ldap = require("resty.ffi.ldap")

return function()
    local client = ldap.new({
        url = "ldap://bonsai.test",
        maxconn = 2,
        auth = {
            mechanism="GSSAPI",
            user="chuck",
            password="Foo2023@",
            realm="BONSAI.TEST",
        }
    })
    assert(client)

    local ok, res = client:search({
        base = "cn=chuck,dc=bonsai,dc=test",
        scope = ldap.SCOPE_SUB,
        filter_exp = "(objectclass=user)",
        attrlist = {'memberOf', 'sAMAccountName'},
    })
    assert(ok)
    res = res[1]
    assert(res.dn == "CN=chuck,DC=bonsai,DC=test", "dn mismatch")
    assert(res.memberOf[1] == "CN=foobar,DC=bonsai,DC=test", "memberOf mismatch")
    assert(res.sAMAccountName[1] == "chuck", "sAMAccountName mismatch")

    local ok = client:close()
    assert(ok)

    ngx.say("ok")
end

nginx.conf

daemon off;
error_log /dev/stderr info;
worker_processes auto;
env LD_LIBRARY_PATH;
env PYTHONPATH;
env KRB5_CONFIG;

events {}

http {
    lua_package_path '/opt/lua-resty-ffi-ldap/demo/?.lua;/opt/lua-resty-ffi-ldap/lua/?.lua;/opt/lua-resty-ffi-ldap/lua/?/init.lua;;';

    server {
        listen 20000;

        location /demo {
            content_by_lua_block {
                require("demo")()
            }
        }
    }
}

Test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# install lua-resty-ffi
# https://github.com/kingluo/lua-resty-ffi#install-lua-resty-ffi-via-luarocks
# set `OR_SRC` to your openresty source path
luarocks config variables.OR_SRC /tmp/tmp.Z2UhJbO1Si/openresty-1.21.4.1
luarocks install lua-resty-ffi

# make lua-resty-ffi python loader library
apt install python3-dev python3-pip libffi-dev
cd /opt
git clone https://github.com/kingluo/lua-resty-ffi
cd /opt/lua-resty-ffi/examples/python
make

apt install libldap2-dev libsasl2-dev heimdal-dev

pip3 install bonsai

cd /opt
git clone https://github.com/kingluo/lua-resty-ffi-ldap

cd /opt/lua-resty-ffi-ldap/demo

# run nginx
KRB5_CONFIG="$PWD/krb5.conf" \
LD_LIBRARY_PATH=/opt/lua-resty-ffi/examples/python:/usr/local/lib/lua/5.1 \
PYTHONPATH=/opt/lua-resty-ffi-ldap \
nginx -p $PWD -c nginx.conf

# in another terminal, trigger demo
curl localhost:20000/demo

Search result in JSON:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[
  {
    "dn":"CN=chuck,DC=bonsai,DC=test",
    "memberOf":[
      "CN=foobar,DC=bonsai,DC=test"
    ],
    "sAMAccountName":[
      "chuck"
    ]
  }
]

Conclusion

With lua-resty-ffi, you could use your favorite mainstream programming language, e.g. Go, Java, Python, Rust, or Node.js, to do development in Openresty/Nginx, so that you could enjoy their rich ecosystem directly.

Welcome to discuss and like my github repo:

https://github.com/kingluo/lua-resty-ffi