Как стать автором
Обновить

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

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

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

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

  • Отсутствие необходимости установки дополнительного ПО
  • Отдельная страница аутентификации
  • Сквозная аутентификация для всех сервисов за nginx
  • Хотя бы минимальная защита от перебора

Вариант с 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:

/etc/nginx/nginx.conf
• • •
http {
• • •
    # Объявляем глобальный контейнер
    lua_shared_dict secure 10m;
    # Инициализируем скрипт
    init_by_lua_file /etc/nginx/lua/secure.lua;
• • •
    include /etc/nginx/conf.d/*.conf;
}


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

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

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

-- Получаем токен и время из cookie
local auth_str = ngx.var.cookie_sv_auth
local auth_token = ""
local life_time = ""

if auth_str ~= nil and auth_str:find("|") ~= nil then
    local divider = auth_str:find("|")
    auth_token = auth_str:sub(0,divider-1)
    life_time = auth_str:sub(divider+1)

    -- 2. Проверяем валидность токена
    if auth_token == ngx.encode_base64(ngx.hmac_sha1("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ",ua.."|"..life_time)) and tonumber(life_time) >= ngx.time() then
        -- Токен валиден
        return
    end
end

-- Токен не валиден или отсутствует
-- 2.1. Сохраняем в coockie url назначения
ngx.header["Set-Cookie"] = "sv_req_url="..ngx.req.get_headers()["Host"].."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()+60*60).."; Secure; HttpOnly"

-- И возвращаем редирект на страницу аутентификации
return ngx.redirect(req_url_err)


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

/etc/nginx/conf.d/plex.conf
server {
    listen                    443 ssl;
    server_name               plex.somedomain.ru;

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

    location / {
        proxy_pass            http://localhost:32400;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
    ssl                       on;
• • •
}


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

/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:

/etc/nginx/conf.d/auth.conf
server {
    listen                    443 ssl;
    server_name               auth.somedomain.ru;

    access_by_lua_file /etc/nginx/lua/auth_access.lua;

    location / {
        default_type    'text/html';
        root            /var/www/html/;
        index            auth.html;
        if ($request_method = POST ) {
            content_by_lua_file /etc/nginx/lua/auth.lua;
        }
    }
    ssl                       on;
• • •
}


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

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

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

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

-- 4. Проверка количества попыток аутентификации
if is_secure(ip,ua,false) then
    -- Проверка пройдена, удаляем невалидный токен
    ngx.header["Set-Cookie"] = {"sv_auth=; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()-60).."; Secure; HttpOnly"}
    return
end

-- 4.2. Проверка не пройдена, возвращаем HTTP 403
ngx.exit(ngx.HTTP_FORBIDDEN)


И проверку логина/пароля с помощью «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.2018 (спасибо YourChief):
— Убрана функция nvl, за ненадобностью
— md5 при генерации токена заменено на HMAC
— В токен добавлено время его жизни
— md5 и HMAC используются встроенные в nginx
Теги:
Хабы:
Всего голосов 28: ↑24 и ↓4+20
Комментарии12

Публикации

Истории

Ближайшие события