
Доброго времени суток. В данной заметке хочу рассказать о простой аутентификации с помощь nginx и lua-скриптов.
Подняв у себя домашний сервер на ubuntu с plex и transmission и обзаведясь доменом, через который вывел это добро в большой мир, понял Я, что было бы неплохо обзавестись единой точкой аутентификации. Тем более nginx у меня уже был установлен (даже nginx-extras, что немаловажно, поскольку там есть lua).
Собравшись с мыслями, сформулировал требования:
- Отсутствие необходимости установки дополнительного ПО
- Отдельная страница аутентификации
- Сквозная аутентификация для всех сервисов за nginx
- Хотя бы минимальная защита от перебора
Вариант с nginx basic auth не устроил по причине отсутствия защиты от перебора, вариант с nginx auth PAM вызвал у меня недоверие по причине аутентификации по логину/паролю ОС. И оба варианта не дают возможности аутентификации через свою отдельную форму.
Алгоритм аутентификации довольно прост:

Ну что ж, приступим.
Для начала создадим 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