Compare commits

..

No commits in common. "2c873ba74d2ed9dc5ba2e024bfa1518e512aebf6" and "3d755031e49ed6d48a10c2098534affb264a9bd6" have entirely different histories.

3 changed files with 14 additions and 49 deletions

View file

@ -89,9 +89,6 @@ 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
@ -113,16 +110,6 @@ 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
@ -141,5 +128,3 @@ location /krbauth {
include uwsgi_params; include uwsgi_params;
} }
``` ```
[limits-storage]: https://limits.readthedocs.io/en/stable/storage.html#storage-scheme

View file

@ -4,7 +4,6 @@ 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
@ -12,10 +11,7 @@ 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__)
@ -23,10 +19,6 @@ 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
@ -41,7 +33,6 @@ 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)
@ -104,7 +95,7 @@ def make_401(reason: str, negotiate: Optional[str] = 'Negotiate', **kwargs) -> R
</body> </body>
</html> </html>
''' % (reason,), status=401) ''' % (reason,), status=401)
if ENABLE_GSSAPI and negotiate: if 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')
@ -141,13 +132,10 @@ 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))' % ( ldap_filter = '(&(memberOf=%s)(krbPrincipalName=%s))' % (context.ldap_group, krb5_name)
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('Authentication failed', krb5_name=krb5_name) return make_401('Did not find LDAP group member', 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)
@ -155,10 +143,6 @@ 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:])
@ -166,8 +150,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 is_sane_username(username) or not password: if not username or not password:
return make_401('Authentication failed') return make_401('Invalid username or password')
assert LDAP_USER_DN is not None assert LDAP_USER_DN is not None
dn = LDAP_USER_DN % (username,) dn = LDAP_USER_DN % (username,)
@ -175,16 +159,14 @@ 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('Authentication failed', dn=dn) return make_401('Failed to authenticate to LDAP', dn=dn)
if context.ldap_group: if context.ldap_group:
if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % ( if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % (context.ldap_group,)):
escape_filter_chars(context.ldap_group), return make_401('Did not find LDAP group member', dn=dn, group=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 from %s', dn, request.remote_addr) app.logger.info('Authenticated via LDAP as: %s', dn)
return auth_success(context, next_url) return auth_success(context, next_url)
@ -197,16 +179,14 @@ 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():
# No cookie required since the check endpoint can trivially verify mTLS. return auth_success(context, next_url)
return redirect(next_url, code=307) if authorization.startswith('Negotiate '):
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)

View file

@ -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.5', version='0.0.4',
py_modules=['nginx_krbauth'], py_modules=['nginx_krbauth'],
install_requires=['flask', 'gssapi', 'python-ldap', 'flask-limiter'], install_requires=['flask', 'gssapi', 'python-ldap'],
author='quantum', author='quantum',
author_email='quantum2048@gmail.com', author_email='quantum2048@gmail.com',