Skip to content

Commit

Permalink
First import
Browse files Browse the repository at this point in the history
  • Loading branch information
elonen committed Jan 29, 2023
0 parents commit b974efb
Show file tree
Hide file tree
Showing 16 changed files with 949 additions and 0 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
54 changes: 54 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[package]
name = "ldap_authz_proxy"
version = "0.1.0"
edition = "2021"

description = "LDAP authorization proxy for authenticated HTTP users"
homepage = "https://github.com/elonen/ldap_authz_proxy"
license = "MIT"
readme = "README.md"


[package.metadata.deb]

maintainer = "Jarno Elonen <[email protected]>"
copyright = "2023, Jarno Elonen <[email protected]>"
section = "unknown"
changelog = "debian/changelog"

depends = "$auto"

extended-description = """\
LDAP authorization server/proxy for use with (e.g.) Nginx.
Extracts usernames from HTTP headers and performs LDAP queries
to authorize them agains Active Directory or other user databases.
"""

maintainer-scripts = "debian"
assets = [
["target/release/ldap_authz_proxy", "usr/bin/", "755"],
["README.md", "usr/share/doc/ldap_authz_proxy/README", "644"],
["test/nginx-site.conf", "usr/share/doc/ldap_authz_proxy/examples/", "644"],
["example.ini", "etc/ldap_authz_proxy.conf", "640"],
]
conf-files = ["/etc/ldap_authz_proxy.conf"]
systemd-units = { enable = false }


[[bin]]
name = "ldap_authz_proxy"
path = "src/main.rs"

[dependencies]
anyhow = "1.0.68"
configparser = "3.0.2"
docopt = "1.1.1"
hyper = { version = "0.14.23", features = ["full"] }
ldap3 = "0.11.1"
lru_time_cache = "0.11.11"
regex = "1.7.1"
sha2 = "0.10.6"
tokio = { version = "1.24.2", features = ["full"] }
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = {version = "0.3.16", features = ["env-filter", "json"] }
49 changes: 49 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# License

The project is dual licensed under the terms of the Apache License, Version 2.0,
and the MIT License. You may obtain copies of the two licenses at

* https://www.apache.org/licenses/LICENSE-2.0 and
* https://opensource.org/licenses/MIT, respectively.

The following two notices apply to every file of the project.

## The Apache License

```
Copyright 2023 Jarno Elonen <[email protected]>
Licensed under the Apache License, Version 2.0 (the “License”); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
```

## The MIT License

```
Copyright 2023 Jarno Elonen <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
186 changes: 186 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# ldap_authz_proxy - LDAP authorization proxy for authenticated HTTP users

Helper that allows Nginx to lookup from Active Directory / LDAP
if a user is authorized to access some resource -- _after_ the user
has been authenticated by some other means (Kerberos, Basic auth, Token
etc).

Technically it is a small HTTP server that reads usernames from request headers and performs
configured LDAP queries with them, returning status 200 if query succeeded or
403 if it failed. Results are cached for a configurable amount of time.

## Configuration

The server is configured with an INI file, such as:

```ini
[default]
ldap_server_url = ldap://dc1.example.test
ldap_conn_timeout = 10.0
ldap_bind_dn = CN=service,CN=Users,DC=example,DC=test
ldap_bind_password = password123
ldap_search_base = DC=example,DC=test

ldap_cache_size = 1024
ldap_cache_time = 30

username_http_header = X-Ldap-Authz-Username

[users]
http_path = /users$
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=ACL_Users,CN=Users,DC=example,DC=test))

[admins]
http_path = /admins$
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=ACL_Admins,CN=Users,DC=example,DC=test))
```

The `[default]` section contains defaults that can be overridden in other sections.
Other sections can have arbitrary names, and they each specify a URL path matching
rule and settings to apply if it matches. The `http_path` value is a regular expression
that is tested against HTTP requests. If it matches, `ldap_query` from that section
is executed after replacing `%USERNAME%` with the username from `username_http_header` HTTP header.
If the LDAP query succeeds, server returns status 200, otherwise 403.

## Building

The server is written in Rust and can be built with `cargo build --release`.
Resulting binary is `target/release/ldap_authz_proxy`.

## Running

The server can be run with `ldap_authz_proxy <configfile>`. Additional
options are available, see `--help` for details.

The executable will stay in foreground, so it is recommended to run it
with a process manager such as `systemd` or `supervisord`. Example
`systemd` service file is included in `debian/service`.

## Security

The server doesn't require any special privileges, and can be run as a
normal user; the example `systemd` service file runs it as `www-data`.

Configuration file contains LDAP bind password(s), so it shouldn't be
world-readable. The server itself doesn't need to be able to write
to the configuration file.

Usernames are quoted before being used in LDAP queries, so they (hopefully)
can't be used to inject arbitrary LDAP queries. In any case, it is recommended
to use a read-only LDAP bind user just in case.

LDAPS is supported (even though the test scripts use plain ldap://), and is
recommended in production.

The server doesn't handle user passwords at all - it only reads usernames from
HTTP headers and performs LDAP queries with them.

## Packaging

The server can be packaged for Debian variants with `cargo install cargo-deb && cargo deb`.
This is the recommended way to install it when applicable.

## Testing

Use `./run-tests.sh` to execute test suite. It requires `docker compose`
and `curl`. The script performs an end-to-end integratiot test with a
real LDAP server (Active Directory in this case, using Samba) and an
Nginx reverse proxy. It spins up necessary containers, and then performs
Curl HTTP requests against Nginx, comparing their HTTP response status codes to
expected values.

## Ngix configuration

See `test/nginx-site.conf` for a simple example where users are authenticated
with the Basic method and then authorized with this server using _auth_request_ directive.

### Kerberos

This software was originally developed for Active Directory auth using
Nginx, so here's a complementary real-world example on how to authenticate users against AD with
Kerberos (spnego-http-auth-nginx-module) and to then authorize them using
_ldap_authz_proxy_:

```nginx
server {
listen 443 ssl;
ssl_certificate /etc/ssl/private/example.com.fullchain.pem;
ssl_certificate_key /etc/ssl/private/example.com.privkey.pem;
server_name www.example.com;
satisfy all; # Require 2 auths: auth_gss (Kerberos) for authn and auth_request (LDAP proxy) for authz
auth_gss on;
auth_gss_keytab /etc/krb5.keytab;
auth_gss_realm EXAMPLE.COM;
auth_gss_force_realm on;
auth_gss_service_name HTTP/www.example.com;
auth_request /authz_all;
proxy_set_header X-Remote-User-Id $remote_user;
proxy_set_header X-Remote-User-Name $remote_user;
location = /authz_all {
internal;
proxy_pass http://127.0.0.1:10567/users;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-Authz-Username $remote_user;
}
location / {
root /var/www/;
index index.html;
try_files $uri $uri/ =404;
}
}
```

The VM running Nginx (and ldap_authz_proxy) was joined to AD domain like this:

```
kinit <account name>
msktutil -u -s host -s HTTP --dont-expire-password -b OU=Servers --computer-name WWW -h www.example.com
setfacl -m u:www-data:r-- /etc/krb5.keytab
```

Some instructions for compiling _spnego-http-auth-nginx-module_ on Debian: https://docs.j7k6.org/sso-nginx-kerberos-spnego-debian/

## Development

Probably the easiest way to develop this is to:

```bash
# Start test LDAP server
cd test
docker compose up -d
cd ..

# Config, build and run
sed -i 's@ldap_server_url *=.*@ldap_server_url = ldap://127.0.0.1:3890@' example.ini
cargo run -- example.ini --debug &

# Test request
curl http://127.0.0.1:10567/admins -H "X-Ldap-Authz-Username:alice"

# Cleanup
kill %1 # Or do: fg + ctrl-c
cd test
docker compose down
cd ..
git checkout -- example.ini # Reverse the config
```

## Contributing

This server was created to scratch a persistent sysop itch.
Contributions are welcome.

## License

Copyright 2023 by Jarno Elonen.

The project is dual licensed under the terms of the Apache License, Version 2.0, and the MIT License.
See LICENSE.md for details.
4 changes: 4 additions & 0 deletions debian/README.deb-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This package uses cargo-deb. Build a package by issuing:

$ cargo install cargo-deb
$ cargo deb
5 changes: 5 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ldap_authz_proxy (0.1-1) unstable; urgency=low

* First release

-- Jarno Elonen <[email protected]> Sat, 28 Jan 2023 23:08:00 +0000
16 changes: 16 additions & 0 deletions debian/postinst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh

# Cargo.toml apparently doesn't allow setting owner/group for files, so we do it here

chown root:www-data /etc/ldap_authz_proxy.conf
chmod 640 /etc/ldap_authz_proxy.conf

if [ ! -f /var/log/ldap_authz_proxy.log ]; then
touch /var/log/ldap_authz_proxy.log
fi
chown root:www-data /var/log/ldap_authz_proxy.log
chmod 660 /var/log/ldap_authz_proxy.log

#DEBHELPER#

exit 0
14 changes: 14 additions & 0 deletions debian/service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=LDAP authz proxy
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=2
User=www-data
ExecStart=/usr/bin/ldap_authz_proxy /etc/ldap_authz_proxy.conf --log /var/log/ldap_authz_proxy.log

[Install]
WantedBy=multi-user.target
19 changes: 19 additions & 0 deletions example.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[default]
ldap_server_url = ldap://dc1.example.test
ldap_conn_timeout = 10.0
ldap_bind_dn = CN=service,CN=Users,DC=example,DC=test
ldap_bind_password = password123
ldap_search_base = DC=example,DC=test

ldap_cache_size = 1024
ldap_cache_time = 30

username_http_header = X-Ldap-Authz-Username

[users]
http_path = /users$
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=ACL_Users,CN=Users,DC=example,DC=test))

[admins]
http_path = /admins$
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=ACL_Admins,CN=Users,DC=example,DC=test))
Loading

0 comments on commit b974efb

Please sign in to comment.