...

пятница, 23 марта 2018 г.

[Из песочницы] Простая аутентификация на NGINX с помощью LUA

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

md5 = require("md5")

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 = md5.sumhexa(ip..user_agent)
    local md5_ip_4 = md5.sumhexa(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 = md5.sumhexa(md5_ip_3)
    md5_ip_2 = md5.sumhexa(md5_ip_2)
    md5_ip_1 = md5.sumhexa(md5_ip_1)
    if is_cnt then
        -- Учитываем неуспешную попытку
        counters["ip_ua"][md5_ip_ua] = nvl(counters["ip_ua"][md5_ip_ua],0) + 1
        counters["ip_4"][md5_ip_4] = nvl(counters["ip_4"][md5_ip_4],0) + 1
        counters["ip_3"][md5_ip_3] = nvl(counters["ip_3"][md5_ip_3],0) + 1
        counters["ip_2"][md5_ip_2] = nvl(counters["ip_2"][md5_ip_2],0) + 1
        counters["ip_1"][md5_ip_1] = nvl(counters["ip_1"][md5_ip_1],0) + 1
        
        -- Пишем в лог подробности неуспешной попытки
        log_file = io.open("/var/log/nginx/access.log", "a")
        log_file:write(ip.."    "..nvl(counters["ip_ua"][md5_ip_ua],0).."    "..nvl(counters["ip_4"][md5_ip_4],0).."    "..nvl(counters["ip_3"][md5_ip_3],0).."    "..nvl(counters["ip_2"][md5_ip_2],0).."    "..nvl(counters["ip_1"][md5_ip_1],0).."    "..user_agent.."\n")
        log_file:close()
    else
        -- Проверяем число неуспешных попыток
        if
            nvl(counters["ip_ua"][md5_ip_ua],0) > ip_ua_max or
            nvl(counters["ip_4"][md5_ip_4],0) > ip_4_max or
            nvl(counters["ip_3"][md5_ip_3],0) > ip_3_max or
            nvl(counters["ip_2"][md5_ip_2],0) > ip_2_max or
            nvl(counters["ip_1"][md5_ip_1],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..":"..md5.sumhexa(pass) then
            auth_file:close()
            return true
        end
    end
    auth_file:close()
    return false
end

-- Просто удобная функция
function nvl(val, def)
    if val ~= nil then
        return val
    else
        return def
    end
end

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


Добавим инициализацию данного скрипта в глобальный конфиг 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
md5 = require("md5")

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

-- Получаем токен из cookie
local sv_auth_ck = ngx.var.cookie_sv_auth

-- 2. Проверяем валидность токена
if sv_auth_ck == md5.sumhexa("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ|"..ngx.req.get_headers()["User-Agent"].."|"..os.date("%x", ngx.time())) then
    -- Токен валиден
    return
else
    -- Токен не валиден (или отсутствует)
    -- 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"
    
    -- 3. И возвращаем редирект на страницу аутентификации
    return ngx.redirect(req_url_err)
end


Добавим проверку данным скриптом в конфиги внутренних сервисов:
/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


И добавим для неё конфиг 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
else
    -- 4.2. Проверка не пройдена, возвращаем HTTP 403
    ngx.exit(ngx.HTTP_FORBIDDEN)
end


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

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

-- Получаем 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://"..nvl(ngx.var.cookie_sv_req_url,"somedomain.ru")

-- Проверяем наличие параметров POST-запроса, если их нет – редирект на страницу аутентификации
ngx.req.read_body()
local args, err = ngx.req.get_post_args()
if not args then
    ngx.redirect(req_url_err);
end

-- 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 or pass == nil then
    ngx.redirect(req_url_err);
else
    -- 5. Проверяем валидны ли логин и пароль
    if sing_in(log, pass) then
        -- Если валидны, генерируем токен
        local auth_str = md5.sumhexa("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ|"..ua.."|"..os.date("%x", ngx.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)
    
    -- 3. И возвращаем редирект на страницу аутентификации
    ngx.redirect(req_url_err)
end



Теперь создадим файл с логином и паролем:
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;

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

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий