📜 ⬆️ ⬇️

Simple authentication on NGINX using LUA

image
Good day. In this article I want to talk about simple authentication using nginx and lua-scripts.

Having picked up a home server on ubuntu with plex and transmission and acquiring a domain through which I brought this good to the big world, I realized that it would be nice to get a single authentication point. Moreover, I already had nginx installed (even nginx-extras, which is important, since lua is there).

Gathering his thoughts, he formulated the requirements:


The variant with nginx did not suit basic auth due to the lack of brute-force protection; the variant with nginx auth PAM caused me to mistrust because of authentication by the OS login / password. And both options do not allow authentication through its separate form.
')
The authentication algorithm is pretty simple:
image

Well, let's get started.

To begin, create a lua script with some functions that we will need in the future:

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


Add initialization of this script to the global nginx config:

/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; } 


Now we will create a lua script for checking the cookie (steps 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) 


Let's add a check with this script to the configs of internal services:

/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; • • • } 


Create an authentication page:

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


And add the nginx config for it:

/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; • • • } 


In this config, we check the number of authentication attempts using “auth_access.lua” (step 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) 


And verification of login / password using "auth.lua" (step 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) 


Now we will create a file with login and password:

 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 

Substituting the login instead of LOGIN, and the password instead of PASSWORD.

That's it, authentication is implemented.

When adding services, it will suffice to specify in the configs the check for “access.lua”:

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

Thanks for attention.

UPD 03/26/2018 (thanks to YourChief ):
- Removed the function nvl, as unnecessary
- md5 replaced by HMAC during token generation
- Token added the time of his life
- md5 and HMAC are used built into nginx

Source: https://habr.com/ru/post/351904/


All Articles