mirror of
https://github.com/quantum5/nginx-krbauth.git
synced 2025-07-26 19:54:13 -04:00
Compare commits
7 commits
3d755031e4
...
2c873ba74d
Author | SHA1 | Date | |
---|---|---|---|
|
2c873ba74d | ||
|
a308005e90 | ||
|
cfa4ff1c52 | ||
|
b9d6630b1a | ||
|
c0395eb97a | ||
|
32cebf4691 | ||
|
5557530ff9 |
15
README.md
15
README.md
|
@ -89,6 +89,9 @@ the client does not support Kerberos. To use this, configure:
|
||||||
There should be one `%s` symbol in this string, which will be replaced by the
|
There should be one `%s` symbol in this string, which will be replaced by the
|
||||||
username.
|
username.
|
||||||
|
|
||||||
|
You may also choose to exclusively use LDAP without using any Kerberos or GSSAPI
|
||||||
|
by setting the environment variable `KRBAUTH_DISABLE_GSSAPI=yes`.
|
||||||
|
|
||||||
### TLS Client Certificate
|
### TLS Client Certificate
|
||||||
|
|
||||||
It's also possible to use client certificates on machines that have them for
|
It's also possible to use client certificates on machines that have them for
|
||||||
|
@ -110,6 +113,16 @@ ssl_client_certificate /path/to/ca.crt;
|
||||||
ssl_verify_client optional;
|
ssl_verify_client optional;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rate limiting
|
||||||
|
|
||||||
|
`nginx-krbauth` supports rate limiting. The rate limiting frequency can be
|
||||||
|
configured by `KRBAUTH_LIMITER_FREQUENCY` environment variable. The default is
|
||||||
|
`10 / 5 minute`, but you can adjust this as needed.
|
||||||
|
|
||||||
|
The rate limiting state is stored in memory. You can use any
|
||||||
|
[storage mechanism][limits-storage] supported by the `limits` PyPI package.
|
||||||
|
Remember to install any dependencies!
|
||||||
|
|
||||||
## Example `nginx.conf`
|
## Example `nginx.conf`
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
|
@ -128,3 +141,5 @@ location /krbauth {
|
||||||
include uwsgi_params;
|
include uwsgi_params;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[limits-storage]: https://limits.readthedocs.io/en/stable/storage.html#storage-scheme
|
||||||
|
|
|
@ -4,6 +4,7 @@ import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -11,7 +12,10 @@ from typing import Optional
|
||||||
import gssapi
|
import gssapi
|
||||||
import ldap
|
import ldap
|
||||||
from flask import Flask, Response, redirect, request
|
from flask import Flask, Response, redirect, request
|
||||||
|
from flask_limiter import Limiter
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
from gssapi.exceptions import BadMechanismError, GSSError, GeneralError
|
from gssapi.exceptions import BadMechanismError, GSSError, GeneralError
|
||||||
|
from ldap.filter import escape_filter_chars
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -19,6 +23,10 @@ app.logger.setLevel(logging.INFO)
|
||||||
app.url_map.add(Rule('/krbauth', endpoint='krbauth.auth'))
|
app.url_map.add(Rule('/krbauth', endpoint='krbauth.auth'))
|
||||||
app.url_map.add(Rule('/krbauth/check', endpoint='krbauth.check'))
|
app.url_map.add(Rule('/krbauth/check', endpoint='krbauth.check'))
|
||||||
|
|
||||||
|
LIMITER_STORAGE = os.environ.get('KRBAUTH_LIMITER_STORAGE', 'memory://')
|
||||||
|
LIMITER_FREQUENCY = os.environ.get('KRBAUTH_LIMITER_FREQUENCY', '10 / 5 minute')
|
||||||
|
limiter = Limiter(get_remote_address, app=app, storage_uri=LIMITER_STORAGE)
|
||||||
|
|
||||||
timestamp = struct.Struct('!q')
|
timestamp = struct.Struct('!q')
|
||||||
hmac_digest = hashlib.sha512
|
hmac_digest = hashlib.sha512
|
||||||
digest_size = hmac_digest().digest_size
|
digest_size = hmac_digest().digest_size
|
||||||
|
@ -33,6 +41,7 @@ LDAP_SEARCH_BASE = os.environ.get('KRBAUTH_LDAP_SEARCH_BASE')
|
||||||
LDAP_USER_DN = os.environ.get('KRBAUTH_LDAP_USER_DN')
|
LDAP_USER_DN = os.environ.get('KRBAUTH_LDAP_USER_DN')
|
||||||
assert not LDAP_USER_DN or LDAP_USER_DN.count('%s') == 1
|
assert not LDAP_USER_DN or LDAP_USER_DN.count('%s') == 1
|
||||||
|
|
||||||
|
ENABLE_GSSAPI = os.environ.get('KRBAUTH_DISABLE_GSSAPI', '0').lower() not in ('1', 'yes')
|
||||||
GSSAPI_NAME = os.environ.get('KRBAUTH_GSSAPI_NAME')
|
GSSAPI_NAME = os.environ.get('KRBAUTH_GSSAPI_NAME')
|
||||||
if GSSAPI_NAME:
|
if GSSAPI_NAME:
|
||||||
gssapi_name = gssapi.Name(GSSAPI_NAME, gssapi.NameType.hostbased_service)
|
gssapi_name = gssapi.Name(GSSAPI_NAME, gssapi.NameType.hostbased_service)
|
||||||
|
@ -95,7 +104,7 @@ def make_401(reason: str, negotiate: Optional[str] = 'Negotiate', **kwargs) -> R
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
''' % (reason,), status=401)
|
''' % (reason,), status=401)
|
||||||
if negotiate:
|
if ENABLE_GSSAPI and negotiate:
|
||||||
resp.headers.add('WWW-Authenticate', negotiate)
|
resp.headers.add('WWW-Authenticate', negotiate)
|
||||||
if LDAP_USER_DN:
|
if LDAP_USER_DN:
|
||||||
resp.headers.add('WWW-Authenticate', 'Basic')
|
resp.headers.add('WWW-Authenticate', 'Basic')
|
||||||
|
@ -132,10 +141,13 @@ def auth_spnego(context: Context, next_url: str) -> Response:
|
||||||
ldap_ctx = ldap.initialize(LDAP_SERVER)
|
ldap_ctx = ldap.initialize(LDAP_SERVER)
|
||||||
if LDAP_BIND_DN and LDAP_BIND_AUTHTOK:
|
if LDAP_BIND_DN and LDAP_BIND_AUTHTOK:
|
||||||
ldap_ctx.bind_s(LDAP_BIND_DN, LDAP_BIND_AUTHTOK, ldap.AUTH_SIMPLE)
|
ldap_ctx.bind_s(LDAP_BIND_DN, LDAP_BIND_AUTHTOK, ldap.AUTH_SIMPLE)
|
||||||
ldap_filter = '(&(memberOf=%s)(krbPrincipalName=%s))' % (context.ldap_group, krb5_name)
|
ldap_filter = '(&(memberOf=%s)(krbPrincipalName=%s))' % (
|
||||||
|
escape_filter_chars(context.ldap_group),
|
||||||
|
escape_filter_chars(str(krb5_name)),
|
||||||
|
)
|
||||||
result = ldap_ctx.search_s(LDAP_SEARCH_BASE, ldap.SCOPE_SUBTREE, ldap_filter, ['cn'])
|
result = ldap_ctx.search_s(LDAP_SEARCH_BASE, ldap.SCOPE_SUBTREE, ldap_filter, ['cn'])
|
||||||
if not result:
|
if not result:
|
||||||
return make_401('Did not find LDAP group member', krb5_name=krb5_name)
|
return make_401('Authentication failed', krb5_name=krb5_name)
|
||||||
app.logger.info('Authenticated via Kerberos as: %s, %s', krb5_name, result[0][0])
|
app.logger.info('Authenticated via Kerberos as: %s, %s', krb5_name, result[0][0])
|
||||||
else:
|
else:
|
||||||
app.logger.info('Authenticated via Kerberos as: %s', krb5_name)
|
app.logger.info('Authenticated via Kerberos as: %s', krb5_name)
|
||||||
|
@ -143,6 +155,10 @@ def auth_spnego(context: Context, next_url: str) -> Response:
|
||||||
return auth_success(context, next_url)
|
return auth_success(context, next_url)
|
||||||
|
|
||||||
|
|
||||||
|
def is_sane_username(username: str) -> bool:
|
||||||
|
return len(username) <= 64 and re.match(r'^[a-zA-Z0-9._@-]+$', username) is not None
|
||||||
|
|
||||||
|
|
||||||
def auth_basic(context: Context, next_url: str) -> Response:
|
def auth_basic(context: Context, next_url: str) -> Response:
|
||||||
try:
|
try:
|
||||||
token = base64.b64decode(request.headers['Authorization'][6:])
|
token = base64.b64decode(request.headers['Authorization'][6:])
|
||||||
|
@ -150,8 +166,8 @@ def auth_basic(context: Context, next_url: str) -> Response:
|
||||||
except (binascii.Error, UnicodeDecodeError):
|
except (binascii.Error, UnicodeDecodeError):
|
||||||
return Response(status=400)
|
return Response(status=400)
|
||||||
|
|
||||||
if not username or not password:
|
if not username or not is_sane_username(username) or not password:
|
||||||
return make_401('Invalid username or password')
|
return make_401('Authentication failed')
|
||||||
|
|
||||||
assert LDAP_USER_DN is not None
|
assert LDAP_USER_DN is not None
|
||||||
dn = LDAP_USER_DN % (username,)
|
dn = LDAP_USER_DN % (username,)
|
||||||
|
@ -159,14 +175,16 @@ def auth_basic(context: Context, next_url: str) -> Response:
|
||||||
try:
|
try:
|
||||||
ldap_ctx.bind_s(dn, password)
|
ldap_ctx.bind_s(dn, password)
|
||||||
except ldap.INVALID_CREDENTIALS:
|
except ldap.INVALID_CREDENTIALS:
|
||||||
return make_401('Failed to authenticate to LDAP', dn=dn)
|
return make_401('Authentication failed', dn=dn)
|
||||||
|
|
||||||
if context.ldap_group:
|
if context.ldap_group:
|
||||||
if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % (context.ldap_group,)):
|
if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % (
|
||||||
return make_401('Did not find LDAP group member', dn=dn, group=context.ldap_group)
|
escape_filter_chars(context.ldap_group),
|
||||||
app.logger.info('Authenticated via LDAP as: %s in %s', dn, context.ldap_group)
|
)):
|
||||||
|
return make_401('Authentication failed', dn=dn, group=context.ldap_group)
|
||||||
|
app.logger.info('Authenticated via LDAP as: %s in %s from %s', dn, context.ldap_group, request.remote_addr)
|
||||||
else:
|
else:
|
||||||
app.logger.info('Authenticated via LDAP as: %s', dn)
|
app.logger.info('Authenticated via LDAP as: %s from %s', dn, request.remote_addr)
|
||||||
|
|
||||||
return auth_success(context, next_url)
|
return auth_success(context, next_url)
|
||||||
|
|
||||||
|
@ -179,14 +197,16 @@ def check_tls() -> bool:
|
||||||
|
|
||||||
|
|
||||||
@app.endpoint('krbauth.auth')
|
@app.endpoint('krbauth.auth')
|
||||||
|
@limiter.limit(LIMITER_FREQUENCY)
|
||||||
def auth() -> Response:
|
def auth() -> Response:
|
||||||
next_url = request.args.get('next', '/')
|
next_url = request.args.get('next', '/')
|
||||||
context = Context.from_request()
|
context = Context.from_request()
|
||||||
authorization = request.headers.get('Authorization', '')
|
authorization = request.headers.get('Authorization', '')
|
||||||
|
|
||||||
if check_tls():
|
if check_tls():
|
||||||
return auth_success(context, next_url)
|
# No cookie required since the check endpoint can trivially verify mTLS.
|
||||||
if authorization.startswith('Negotiate '):
|
return redirect(next_url, code=307)
|
||||||
|
if ENABLE_GSSAPI and authorization.startswith('Negotiate '):
|
||||||
return auth_spnego(context, next_url)
|
return auth_spnego(context, next_url)
|
||||||
if LDAP_USER_DN and authorization.startswith('Basic '):
|
if LDAP_USER_DN and authorization.startswith('Basic '):
|
||||||
return auth_basic(context, next_url)
|
return auth_basic(context, next_url)
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -7,9 +7,9 @@ with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='nginx_krbauth',
|
name='nginx_krbauth',
|
||||||
version='0.0.4',
|
version='0.0.5',
|
||||||
py_modules=['nginx_krbauth'],
|
py_modules=['nginx_krbauth'],
|
||||||
install_requires=['flask', 'gssapi', 'python-ldap'],
|
install_requires=['flask', 'gssapi', 'python-ldap', 'flask-limiter'],
|
||||||
|
|
||||||
author='quantum',
|
author='quantum',
|
||||||
author_email='quantum2048@gmail.com',
|
author_email='quantum2048@gmail.com',
|
||||||
|
|
Loading…
Reference in a new issue