GitHub – stnoonan/spnego- SPNEGO HTTP Authentication Module for nginx

Basic authentication fallback

The module falls back to basic authentication by default if no negotiation is
attempted by the client. If you are using SPNEGO without SSL, it is recommended
you disable basic authentication fallback, as the password would be sent in
plaintext. This is done by setting auth_gss_allow_basic_fallback in the
config file.

These options affect the operation of basic authentication:

Configuration reference

You can configure GSS authentication on a per-location and/or a global basis:

These options are required.

  • auth_gss: on/off, for ease of unsecuring while leaving other options in
    the config file
  • auth_gss_keytab: absolute path-name to keytab file containing service
    credentials

These options should ONLY be specified if you have a keytab containing
privileged principals. In nearly all cases, you should not put these
in the configuration file, as gss_accept_sec_context will do the right
thing.

Debugging

The module prints all sort of debugging information if nginx is compiled with
the –with-debug option, and the error_log directive has a debug level.

Prerequisites

Authentication has been tested with (at least) the following:

  • Nginx 1.2 through 1.7
  • Internet Explorer 8 and above
  • Firefox 10 and above
  • Chrome 20 and above
  • Curl 7.x (GSS-Negotiate), 7.x (SPNEGO/fbopenssl)

The underlying kerberos library used for these tests was MIT KRB5 v1.8.

Простая аутентификация на nginx с помощью lua

image

Доброго времени суток. В данной заметке хочу рассказать о простой аутентификации с помощь nginx и lua-скриптов.

Подняв у себя домашний сервер на ubuntu с plex и transmission и обзаведясь доменом, через который вывел это добро в большой мир, понял Я, что было бы неплохо обзавестись единой точкой аутентификации. Тем более nginx у меня уже был установлен (даже nginx-extras, что немаловажно, поскольку там есть lua).

Собравшись с мыслями, сформулировал требования:

Вариант с nginx basic auth не устроил по причине отсутствия защиты от перебора, вариант с nginx auth PAM вызвал у меня недоверие по причине аутентификации по логину/паролю ОС. И оба варианта не дают возможности аутентификации через свою отдельную форму.

Алгоритм аутентификации довольно прост:
image

Ну что ж, приступим.

Для начала создадим lua-скрипт с некоторым функциями, которые понадобятся нам в дальнейшем:

/etc/nginx/lua/secure.lua

-- Количество попыток для ip/32 и User-Agent
local ip_ua_max = 10

-- Количество попыток для ip/32
local ip_4_max = 50

-- Количество попыток для ip/16
local ip_3_max = 100

-- Количество попыток для ip/8
local ip_2_max = 500

-- Количество попыток для ip/0
local ip_1_max = 1000

counters = {}
counters["ip_ua"] = {}
counters["ip_4"] = {}
counters["ip_3"] = {}
counters["ip_2"] = {}
counters["ip_1"] = {}

-- Проверка числа попыток (is_cnt=false) и учёт неуспешной попытки (is_cnt=true)
function is_secure(ip, user_agent, is_cnt)
    local md5_ip_ua = ngx.md5(ip..user_agent)
    local md5_ip_4 = ngx.md5(ip)
    local md5_ip_3 = ""
    local md5_ip_2 = ""
    local md5_ip_1 = ""
    local cnt = 0
    for i in string.gmatch(ip, "%d ") do
        cnt = cnt   1
        if cnt < 4 then
            md5_ip_3 = md5_ip_3.."."..i
        end
        if cnt < 3 then
            md5_ip_2 = md5_ip_2.."."..i
        end
        if cnt < 2 then
            md5_ip_1 = md5_ip_1.."."..i
        end
    end
    md5_ip_3 = ngx.md5(md5_ip_3)
    md5_ip_2 = ngx.md5(md5_ip_2)
    md5_ip_1 = ngx.md5(md5_ip_1)
    if is_cnt then
        -- Учитываем неуспешную попытку
        counters["ip_ua"][md5_ip_ua] = (counters["ip_ua"][md5_ip_ua] or 0)   1
        counters["ip_4"][md5_ip_4] = (counters["ip_4"][md5_ip_4] or 0)   1
        counters["ip_3"][md5_ip_3] = (counters["ip_3"][md5_ip_3] or 0)   1
        counters["ip_2"][md5_ip_2] = (counters["ip_2"][md5_ip_2] or 0)   1
        counters["ip_1"][md5_ip_1] = (counters["ip_1"][md5_ip_1] or 0)   1
        
        -- Пишем в лог подробности неуспешной попытки
        log_file = io.open("/var/log/nginx/access.log", "a")
        log_file:write(ip.."	"..(counters["ip_ua"][md5_ip_ua] or 0).."	"..(counters["ip_4"][md5_ip_4] or 0).."	"..(counters["ip_3"][md5_ip_3] or 0).."	"..(counters["ip_2"][md5_ip_2] or 0).."	"..(counters["ip_1"][md5_ip_1] or 0).."	"..user_agent.."n")
        log_file:close()
    else
        -- Проверяем число неуспешных попыток
        if
            (counters["ip_ua"][md5_ip_ua] or 0) > ip_ua_max or
            (counters["ip_4"][md5_ip_4] or 0) > ip_4_max or
            (counters["ip_3"][md5_ip_3] or 0) > ip_3_max or
            (counters["ip_2"][md5_ip_2] or 0) > ip_2_max or
            (counters["ip_1"][md5_ip_1] or 0) > ip_1_max
        then
            return false
        else
            return true
        end
    end
end

-- Проверка логина/пароля
-- В данном примере просто сравнение с хэшом из файла, при желании в данной функции можно реализовать проверку логина/пароля где угодно (в БД например)
function sing_in(log, pass)
    local auth_file = io.open("/etc/nginx/auth/pass","r")
    for line in io.lines("/etc/nginx/auth/pass") do
        if line == log..":"..ngx.md5(pass) then
            auth_file:close()
            return true
        end
    end
    auth_file:close()
    return false
end

-- Сохраняем функции в глобальном контейнере secure
local secure = ngx.shared.secure
secure:set("sing_in", sing_in)
secure:set("is_secure", is_secure)

Добавим инициализацию данного скрипта в глобальный конфиг nginx:

Теперь создадим lua-скрипт для проверки cookie (шаги 2, 2.1, 3):

Добавим проверку данным скриптом в конфиги внутренних сервисов:

Создадим страницу аутентификации:

/var/www/html/auth.html

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8">
        <title>somedomain</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body{
                height: 100%;
                background-color: rgb(64, 64, 64);
                text-align:center;
                align:center;
                vertical-align: middle;
            }
            form {
                display: inline-block;
                text-align: center;
                vertical-align: middle;
                position:absolute;
                top:50%;
                right:0;
                left:0;
            }
            input{
                color: rgb(0, 255, 0);
                text-align: center; 
                border: 2px solid;
                border-color: rgb(0, 255, 0);
                background-color: rgb(64, 64, 64);
            }
            ::-webkit-input-placeholder{
                color:rgb(0, 255, 0);
                text-align: center;
            }
            ::-moz-placeholder{
                color:rgb(0, 255, 0);
                text-align: center;
            }
            :-moz-placeholder{
                color:rgb(0, 255, 0);
                text-align: center;
            }
            :-ms-input-placeholder{
                color:rgb(0, 255, 0);
                text-align: center;
            }
            br{
                display: block;
                margin: 7px 0;
                line-height: 7px;
                content: " ";
            }
        </style>
    </head>
    <body>
        <form method="post">
            <input type="text" name="login" placeholder="login" autocomplete="off">
            <br>
            <input type="password" name="password" placeholder="password" autocomplete="off">
            <br>
            <input type="submit" value="sign in">
        </form>
    </body>
</html>

И добавим для неё конфиг nginx:

В данном конфиге делаем проверку числа попыток аутентификации с помощью «auth_access.lua» (шаг 4, 4.2)

И проверку логина/пароля с помощью «auth.lua» (шаг 5, 5.1, 2.2)

/etc/nginx/lua/auth.lua

-- Берём из глобального контейнера secure нужные нам функции
local secure = ngx.shared.secure
sing_in = secure:get("sing_in")
is_secure = secure:get("is_secure")

-- Получаем ip адрес клиента
local ip = ngx.var.remote_addr

-- Получаем User-Agent адрес клиента
local ua = ngx.req.get_headers()["User-Agent"]

-- Адрес страницы аутентификации
local req_url_err = "https://auth.somedomain.ru"

-- Адрес назначения из cookie или дефолтный адрес, если в cookie адреса нет
local req_url = "https://"..(ngx.var.cookie_sv_req_url or "somedomain.ru")

-- Проверяем наличие параметров POST-запроса
ngx.req.read_body()
local args, err = ngx.req.get_post_args()
if args then
    -- 4.1. Читаем из POST-запроса логин и пароль
    local log
    local pass
    for key, val in pairs(args) do
        if key == "login" then
            log = val
        elseif key == "password" then
            pass = val
        end
    end

    -- Проверяем, что логин и пароль не пустые
    if log ~= nil and pass ~= nil then
        -- 5. Проверяем валидны ли логин и пароль
        if sing_in(log, pass) then
            -- Если валидны
            -- Задаём время жизни токена (сутки)
            local life_time = ngx.time() 86400
            -- Генерируем токен
            local auth_str = ngx.encode_base64(ngx.hmac_sha1("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ",ua.."|"..life_time)).."|"..life_time
            
            -- 5.1. Записываем токен в cookie и удаляем оттуда url назначения
            ngx.header["Set-Cookie"] = {"sv_auth="..auth_str.."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time() 60*60*24).."; Secure; HttpOnly","sv_req_url="..ngx.req.get_headers()["Host"].."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()-60).."; Secure; HttpOnly"}
            
            -- 2.2. Возвращаем редирект на страницу назначения
            return ngx.redirect(req_url)
        end
        
        -- 5.2. Если логин/пароль невалидны, учитываем это в подсчёте неуспешных попыток аутентификации
        is_secure(ip,ua,true)
    end
end

-- 3. Если логин и пароль не переданы или невалидны, возвращаем редирект на страницу аутентификации
ngx.redirect(req_url_err)

Теперь создадим файл с логином и паролем:

md5="`echo -n "PASSWORD" | md5sum`";echo -e "LOGIN"":`sed 's/^([^ ] ) .*$/1/' <<< "$md5"`" > ~/pass; sudo mv ~/pass /etc/nginx/auth/pass; sudo chown nginx:nginx /etc/nginx/auth/pass

Подставив вместо «LOGIN» логин, а вместо «PASSWORD» пароль.

Вот и всё, аутентификации реализована.

При добавлении сервисов, достаточно будет в конфигах указывать проверку по «access.lua»:

access_by_lua_file /etc/nginx/lua/access.lua;

Спасибо за внимание.

UPD 26.03.2022 (спасибо YourChief):
— Убрана функция nvl, за ненадобностью
— md5 при генерации токена заменено на HMAC
— В токен добавлено время его жизни
— md5 и HMAC используются встроенные в nginx

Рецепты nginx: basic авторизация с капчей

Для приготовления авторизации с капчей нам понадобится сам

nginx

и его плагины

encrypted-session

,

form-input

,

ctpp2

,

echo

,

headers-more

,

auth_request

,

auth_basic

,

set-misc

. (Я дал ссылки на свои форки, т.к. делал некоторые изменения, которые пока не удалось пропихнуть в оригинальные репозитории. Можно также воспользоваться

готовым образом

.)

Для начала зададим

encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456";

Дальше, на всякий случай, отключаем авторизационный заголовок

more_clear_input_headers Authorization;

Теперь защищаем всё авторизацией

auth_request /auth;
location =/auth {
    internal;
    subrequest_access_phase on; # разрешаем авторизационную фазу в подзапросе
    auth_request off; # не использовать авторизацию
    set_decode_base64 $auth_decode $cookie_auth; # раскодируем авторизационную куку
    set_decrypt_session $auth_decrypt $auth_decode; # расшифровываем авторизационную куку
    if ($auth_decrypt = "") { return 401 UNAUTHORIZED; } # если не удалось расшифровать, то значит пользователь не авторизован
    more_set_input_headers "Authorization: Basic $auth_decrypt"; # подменить авторизацию на basic (чтобы использовать переменную $remote_user)
    auth_basic_user_file /data/nginx/.htaccess; # задаём файл basic авторизации
    auth_basic Auth; # включаем basic авторизацию
    echo -n OK; # пользователь авторизован
}

Для авторизованных пользователей показываем контент из их папки

location / {
    alias html/$remote_user/;
}

А при отсутствии авторизации показываем авторизационную форму с капчей

error_page 401 = @error401;
location @error401 {
    set_escape_uri $request_uri_escape $request_uri; # кодируем запрос
    return 303 /login?request_uri=$request_uri_escape; # перенаправляем на авторизационную форму с капчей, сохранив запрос
}
location =/login {
    default_type "text/html; charset=utf-8"; # задаём тип
    if ($request_method = GET) { # если только показать авторизационную форму с капчей
        template login.html.ct2; # задаём шаблон
        ctpp2 on; # включаем шаблонизатор
        set_secure_random_alphanum $csrf_random 32; # задаём случайное csrf
        encrypted_session_expires 300; # задаём время жизни csrf 5 минут (5 * 60 = 300)
        set_encrypt_session $csrf_encrypt $csrf_random; # зашифровываем случайное csrf
        set_encode_base64 $csrf_encode $csrf_encrypt; # кодируем зашифрованное csrf
        add_header Set-Cookie "CSRF=$csrf_encode; Max-Age=300"; # помещаем зашифрованное csrf в куку на 5 минут (5 * 60 = 300)
        return 200 "{"csrf":"$csrf_random"}"; # возвращаем json для шаблонизатора
    } # иначе - обработать авторизационную форму с капчей
    set_form_input $csrf_form csrf; # получаем csrf из формы
    set_unescape_uri $csrf_unescape $csrf_form; # раскодируем csrf из формы
    set_decode_base64 $csrf_decode $cookie_csrf; # раскодируем csrf из куки
    set_decrypt_session $csrf_decrypt $csrf_decode; # расшифровываем csrf из куки
    if ($csrf_decrypt != $csrf_unescape) { return 303 $request_uri; } # если csrf из формы не совпадает с csrf из куки, то перенаправить на показ формы снова
    set_form_input $captcha_form captcha; # получаем капчу из формы
    set_unescape_uri $captcha_unescape $captcha_form; # раскодируем капчу из формы
    set_md5 $captcha_md5 "secret${captcha_unescape}${csrf_decrypt}"; # считаем md5
    if ($captcha_md5 != $cookie_captcha) { return 303 $request_uri; } # если md5 не совпадает с капчей из куки, то перенаправить на показ формы снова
    set_form_input $username_form username; # получаем логин из формы
    set_form_input $password_form password; # получаем пароль из формы
    set_unescape_uri $username_unescape $username_form; # раскодируем логин из формы
    set_unescape_uri $password_unescape $password_form; # раскодируем пароль из формы
    encrypted_session_expires 2592000; # задаём время жизни сессии 30 дней (30 * 24 * 60 * 60 = 2592000)
    set $username_password "$username_unescape:$password_unescape"; # задаём basic авторизацию
    set_encode_base64 $username_password_encode $username_password; # кодируем basic авторизацию
    set_encrypt_session $auth_encrypt $username_password_encode; # зашифровываем basic авторизацию
    set_encode_base64 $auth_encode $auth_encrypt; # кодируем зашифрованную basic авторизацию
    add_header Set-Cookie "Auth=$auth_encode; Max-Age=2592000"; # помещаем зашифрованную basic авторизацию в авторизационную куку на 30 дней (30 * 24 * 60 * 60 = 2592000)
    set $arg_request_uri_or_slash $arg_request_uri; # копируем запрос из аргумента
    set_if_empty $arg_request_uri_or_slash "/"; # если аргумент не задан, то начало
    set_unescape_uri $request_uri_unescape $arg_request_uri_or_slash; # раскодируем запрос
    return 303 $request_uri_unescape; # перенаправляем на сохранённый запрос
}

login.html

<html>
    <body>
        <form method="post">
            <input type="hidden" name="csrf" value="<TMPL_var csrf>" />
            username: <input type="text" name="username" placeholder="Enter User Name..." /><br />
            password: <input type="password" name="password" /><br />
            captcha: <img src="/captcha?csrf=<TMPL_var csrf>"/><input type="text" name="captcha" autocomplete="off" /><br />
            <input type="submit" name="submit" value="submit" />
        </form>
    </body>
</html>
Похожее:  Use nginx to Add Authentication to Any Application | Okta Developer

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *