Use HAProxy with Lua to enforce dynamically generated passwords for basic authentication.
The idea
The idea is to incorporate daily tokens into a hashing algorithm to ensure that different password hash is used every day.
HAProxy configuration
Users
Create a user/passwords map.
$ sudo -u haproxy cat /etc/haproxy/users.map
milosz notsossecretpassword shared somesharedpassword
Tokens
Generate tokens map.
$ for day in $(seq -w 1 365); do echo "$(date -d "01/01/2020 + $day day" +"%m%d") $(shuf -n 1 /usr/share/dict/american-english)"; done | sudo tee /etc/haproxy/tokens.map
0102 haggle's 0103 navigability 0104 substantiates 0105 deacon's 0106 agent's 0107 meningitis 0108 marshmallow's 0109 Spahn 0110 undoing 0111 Nunavut's 0112 bankroll 0113 driblets 0114 England 0115 seismologist's 0116 Burks's 0117 evisceration's 0118 Elisha 0119 sauerkraut's 0120 anesthesiologists 0121 Yalta's 0122 Hollerith 0123 dorsal 0124 chore 0125 stratosphere 0126 dawdler 0127 Hilton 0128 cooler 0129 pixie 0130 should 0131 name 0201 birettas 0202 ease 0203 scrimshaw 0204 finickiest 0205 torus 0206 boastful 0207 stingray 0208 Orpheus's 0209 subordinates 0210 unprincipled 0211 falseness 0212 varnishes 0213 forwent 0214 concertos 0215 earplugs 0216 anatomists 0217 weeps 0218 croup 0219 dieter 0220 toddling 0221 Hague's 0222 xylem's 0223 Tsiolkovsky 0224 sister's 0225 slurred 0226 foretasting 0227 curlew 0228 discommode 0229 rabbinate 0301 removable 0302 black 0303 Audubon's 0304 Amway's 0305 debt 0306 obligate 0307 Mennonite's 0308 salutes 0309 contraventions 0310 saccharine 0311 hoeing 0312 Bisquick's 0313 sanctification's 0314 prominently 0315 shock's 0316 wavering 0317 disproportion 0318 basically 0319 dialectic's 0320 gigabytes 0321 embroideries 0322 dodger 0323 copeck's 0324 huffing 0325 swordfishes 0326 dolt 0327 Sprint's 0328 apogee's 0329 Bridgette 0330 philosophically 0331 registrar 0401 hostelling 0402 xenophobia's 0403 sidelight 0404 frequenter 0405 outmanoeuvring 0406 Yuan 0407 Jaipur's 0408 camped 0409 gauntness's 0410 proverbially 0411 sprawling 0412 warmhearted 0413 select 0414 abate 0415 dwellers 0416 wristwatch 0417 palette 0418 Xenophon's 0419 stringy 0420 boardwalks 0421 hooter's 0422 cricketer's 0423 polarization 0424 exorcist's 0425 Sade 0426 Morita 0427 steeps 0428 wariest 0429 pursuant 0430 balloonists 0501 calypsos 0502 Rickover 0503 Juno 0504 codicils 0505 fiberboard's 0506 hitter's 0507 flirt's 0508 goldfishes 0509 outbalancing 0510 confound 0511 piety's 0512 knitting 0513 exclamations 0514 petard 0515 scaffolding's 0516 blasphemous 0517 petrol's 0518 subservient 0519 uterus's 0520 zithers 0521 vexation 0522 praising 0523 refracted 0524 ascribes 0525 custodians 0526 Thames 0527 purloined 0528 abnegate 0529 raffles 0530 collations 0531 civility's 0601 duke 0602 reactivated 0603 Sheldon 0604 fusing 0605 glance's 0606 deprogrammed 0607 wearer's 0608 polonium 0609 allowance's 0610 fearful 0611 surfeits 0612 Hasbro 0613 Imogene's 0614 Judson's 0615 Nordic 0616 Joyce's 0617 myrrh's 0618 viewpoint's 0619 elemental 0620 mumps's 0621 Cortland's 0622 fiddle's 0623 obtuse 0624 servomechanism 0625 ogres 0626 homosexual 0627 Themistocles's 0628 plasma's 0629 equinoctial 0630 churns 0701 plough's 0702 subscripts 0703 Elisha 0704 nonintervention 0705 campaigner 0706 leitmotif 0707 wake 0708 plumber 0709 stomachaches 0710 impersonated 0711 prerogatives 0712 completion 0713 opus's 0714 alights 0715 drunkest 0716 Kronecker 0717 pessimistic 0718 itchier 0719 transepts 0720 soy's 0721 Latin 0722 Weill's 0723 dryness's 0724 fricassees 0725 summer's 0726 pacification's 0727 gardens 0728 Kristy 0729 staircases 0730 sociopath's 0731 manifolding 0801 precipitation 0802 Thieu 0803 Calhoun's 0804 Guelph 0805 unfurl 0806 Degas's 0807 Muenster 0808 transceiver 0809 freebased 0810 canvas's 0811 dwells 0812 fortifies 0813 baby's 0814 anaesthetized 0815 theoreticians 0816 timider 0817 shackle 0818 Walters's 0819 constraining 0820 Praia 0821 revoked 0822 performed 0823 Rogers's 0824 heppest 0825 Capitoline 0826 marveling 0827 famine 0828 infirm 0829 caravan 0830 obliterates 0831 adolescents 0901 choker 0902 Ada 0903 waggish 0904 caliper's 0905 tracing's 0906 lambasts 0907 Mafioso's 0908 milestone's 0909 chinks 0910 insensibility 0911 scuff's 0912 winters 0913 seize 0914 animism's 0915 Albuquerque's 0916 coating 0917 isles 0918 spoon 0919 fascinate 0920 appliqués 0921 nosh's 0922 Gruyère 0923 votary 0924 infecting 0925 pretext 0926 writhing 0927 refugee 0928 epithet 0929 flaw's 0930 cyclotron's 1001 angleworm 1002 Highlanders 1003 Myles 1004 occupied 1005 mimeographing 1006 crusade 1007 wisterias 1008 blares 1009 systematizes 1010 organizers 1011 Wheatstone 1012 grieved 1013 expended 1014 motif's 1015 acceding 1016 Lean's 1017 sobers 1018 betrayer 1019 itchiness 1020 depreciation 1021 scoreboards 1022 Cara 1023 overnights 1024 deflections 1025 muckrake 1026 inexhaustible 1027 mandatory 1028 backdating 1029 motorists 1030 locks 1031 Mia's 1101 Corleone's 1102 rumblings 1103 adverb 1104 identify 1105 agglomerates 1106 buyer 1107 linguistics's 1108 cashmere 1109 letdown's 1110 deforests 1111 Fotomat's 1112 grey's 1113 counsels 1114 buttock 1115 huntress 1116 Daumier's 1117 drug's 1118 deerskin 1119 verbals 1120 irritant 1121 formal 1122 blitz's 1123 malnourished 1124 humanity 1125 paddle's 1126 comprising 1127 perforation 1128 Wooster 1129 print 1130 interconnections 1201 mined 1202 Maribel 1203 hags 1204 plumpness 1205 Moroccan's 1206 scimitar 1207 motile 1208 gasping 1209 cutlet's 1210 overplaying 1211 Rolando 1212 Inglewood 1213 Cochabamba's 1214 tabulation 1215 avuncular 1216 Summers's 1217 rewind 1218 TelePrompter 1219 megahertz 1220 shroud's 1221 fore 1222 nukes 1223 droppings 1224 Grafton's 1225 roasters 1226 bankbooks 1227 toddled 1228 arise 1229 Andrei 1230 languished 1231 blabbermouth
Hash functions
Create a Lua library to perform hashing operations.
$ cat /usr/local/share/lua/5.3/sha256.lua
-- SHA-256 code in Lua 5.2; based on the pseudo-code from -- Wikipedia (http://en.wikipedia.org/wiki/SHA-2) local band, rrotate, bxor, rshift, bnot = bit32.band, bit32.rrotate, bit32.bxor, bit32.rshift, bit32.bnot local string, setmetatable, assert = string, setmetatable, assert _ENV = nil -- Initialize table of round constants -- (first 32 bits of the fractional parts of the cube roots of the first -- 64 primes 2..311): local k = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, } -- transform a string of bytes in a string of hexadecimal digits local function str2hexa (s) local h = string.gsub(s, ".", function(c) return string.format("%02x", string.byte(c)) end) return h end -- transform number 'l' in a big-endian sequence of 'n' bytes -- (coded as a string) local function num2s (l, n) local s = "" for i = 1, n do local rem = l % 256 s = string.char(rem) .. s l = (l - rem) / 256 end return s end -- transform the big-endian sequence of four bytes starting at -- index 'i' in 's' into a number local function s232num (s, i) local n = 0 for i = i, i + 3 do n = n*256 + string.byte(s, i) end return n end -- append the bit '1' to the message -- append k bits '0', where k is the minimum number >= 0 such that the -- resulting message length (in bits) is congruent to 448 (mod 512) -- append length of message (before pre-processing), in bits, as 64-bit -- big-endian integer local function preproc (msg, len) local extra = -(len + 1 + 8) % 64 len = num2s(8 * len, 8) -- original len in bits, coded msg = msg .. "\128" .. string.rep("\0", extra) .. len assert(#msg % 64 == 0) return msg end local function initH224 (H) -- (second 32 bits of the fractional parts of the square roots of the -- 9th through 16th primes 23..53) H[1] = 0xc1059ed8 H[2] = 0x367cd507 H[3] = 0x3070dd17 H[4] = 0xf70e5939 H[5] = 0xffc00b31 H[6] = 0x68581511 H[7] = 0x64f98fa7 H[8] = 0xbefa4fa4 return H end local function initH256 (H) -- (first 32 bits of the fractional parts of the square roots of the -- first 8 primes 2..19): H[1] = 0x6a09e667 H[2] = 0xbb67ae85 H[3] = 0x3c6ef372 H[4] = 0xa54ff53a H[5] = 0x510e527f H[6] = 0x9b05688c H[7] = 0x1f83d9ab H[8] = 0x5be0cd19 return H end local function digestblock (msg, i, H) -- break chunk into sixteen 32-bit big-endian words w[1..16] local w = {} for j = 1, 16 do w[j] = s232num(msg, i + (j - 1)*4) end -- Extend the sixteen 32-bit words into sixty-four 32-bit words: for j = 17, 64 do local v = w[j - 15] local s0 = bxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) v = w[j - 2] local s1 = bxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) w[j] = w[j - 16] + s0 + w[j - 7] + s1 end -- Initialize hash value for this chunk: local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] -- Main loop: for i = 1, 64 do local s0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) local maj = bxor(band(a, b), band(a, c), band(b, c)) local t2 = s0 + maj local s1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) local ch = bxor (band(e, f), band(bnot(e), g)) local t1 = h + s1 + ch + k[i] + w[i] h = g g = f f = e e = d + t1 d = c c = b b = a a = t1 + t2 end -- Add (mod 2^32) this chunk's hash to result so far: H[1] = band(H[1] + a) H[2] = band(H[2] + b) H[3] = band(H[3] + c) H[4] = band(H[4] + d) H[5] = band(H[5] + e) H[6] = band(H[6] + f) H[7] = band(H[7] + g) H[8] = band(H[8] + h) end local function finalresult224 (H) -- Produce the final hash value (big-endian): return str2hexa(num2s(H[1], 4)..num2s(H[2], 4)..num2s(H[3], 4)..num2s(H[4], 4).. num2s(H[5], 4)..num2s(H[6], 4)..num2s(H[7], 4)) end local function finalresult256 (H) -- Produce the final hash value (big-endian): return str2hexa(num2s(H[1], 4)..num2s(H[2], 4)..num2s(H[3], 4)..num2s(H[4], 4).. num2s(H[5], 4)..num2s(H[6], 4)..num2s(H[7], 4)..num2s(H[8], 4)) end ---------------------------------------------------------------------- local HH = {} -- to reuse local function hash224 (msg) msg = preproc(msg, #msg) local H = initH224(HH) -- Process the message in successive 512-bit (64 bytes) chunks: for i = 1, #msg, 64 do digestblock(msg, i, H) end return finalresult224(H) end local function hash256 (msg) msg = preproc(msg, #msg) local H = initH256(HH) -- Process the message in successive 512-bit (64 bytes) chunks: for i = 1, #msg, 64 do digestblock(msg, i, H) end return finalresult256(H) end ---------------------------------------------------------------------- local mt = {} local function new256 () local o = {H = initH256({}), msg = "", len = 0} setmetatable(o, mt) return o end mt.__index = mt function mt:add (m) self.msg = self.msg .. m self.len = self.len + #m local t = 0 while #self.msg - t >= 64 do digestblock(self.msg, t + 1, self.H) t = t + 64 end self.msg = self.msg:sub(t + 1, -1) end function mt:close () self.msg = preproc(self.msg, self.len) self:add("") return finalresult256(self.H) end ---------------------------------------------------------------------- return { hash224 = hash224, hash256 = hash256, new256 = new256, }
Base64 functions
Create a Lua library to perform Base64 operations.
$ cat /usr/local/share/lua/5.3/base64.lua
-- #!/usr/bin/env lua -- working lua base64 codec (c) 2006-2008 by Alex Kloss -- compatible with lua 5.1 -- http://www.it-rfc.de -- licensed under the terms of the LGPL2 -- bitshift functions (<<, >> equivalent) -- shift left function lsh(value,shift) return (value*(2^shift)) % 256 end -- shift right function rsh(value,shift) return math.floor(value/2^shift) % 256 end -- return single bit (for OR) function bit(x,b) return (x % 2^b - x % 2^(b-1) > 0) end -- logic OR for number values function lor(x,y) result = 0 for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and 2^(p-1) or 0) end return result end -- encryption table local base64chars = {[0]='A',[1]='B',[2]='C',[3]='D',[4]='E',[5]='F',[6]='G',[7]='H',[8]='I',[9]='J',[10]='K',[11]='L',[12]='M',[13]='N',[14]='O',[15]='P',[16]='Q',[17]='R',[18]='S',[19]='T',[20]='U',[21]='V',[22]='W',[23]='X',[24]='Y',[25]='Z',[26]='a',[27]='b',[28]='c',[29]='d',[30]='e',[31]='f',[32]='g',[33]='h',[34]='i',[35]='j',[36]='k',[37]='l',[38]='m',[39]='n',[40]='o',[41]='p',[42]='q',[43]='r',[44]='s',[45]='t',[46]='u',[47]='v',[48]='w',[49]='x',[50]='y',[51]='z',[52]='0',[53]='1',[54]='2',[55]='3',[56]='4',[57]='5',[58]='6',[59]='7',[60]='8',[61]='9',[62]='-',[63]='_'} -- function encode -- encodes input string to base64. function enc(data) local bytes = {} local result = "" for spos=0,string.len(data)-1,3 do for byte=1,3 do bytes[byte] = string.byte(string.sub(data,(spos+byte))) or 0 end result = string.format('%s%s%s%s%s',result,base64chars[rsh(bytes[1],2)],base64chars[lor(lsh((bytes[1] % 4),4), rsh(bytes[2],4))] or "=",((#data-spos) > 1) and base64chars[lor(lsh(bytes[2] % 16,2), rsh(bytes[3],6))] or "=",((#data-spos) > 2) and base64chars[(bytes[3] % 64)] or "=") end return result end -- decryption table local base64bytes = {['A']=0,['B']=1,['C']=2,['D']=3,['E']=4,['F']=5,['G']=6,['H']=7,['I']=8,['J']=9,['K']=10,['L']=11,['M']=12,['N']=13,['O']=14,['P']=15,['Q']=16,['R']=17,['S']=18,['T']=19,['U']=20,['V']=21,['W']=22,['X']=23,['Y']=24,['Z']=25,['a']=26,['b']=27,['c']=28,['d']=29,['e']=30,['f']=31,['g']=32,['h']=33,['i']=34,['j']=35,['k']=36,['l']=37,['m']=38,['n']=39,['o']=40,['p']=41,['q']=42,['r']=43,['s']=44,['t']=45,['u']=46,['v']=47,['w']=48,['x']=49,['y']=50,['z']=51,['0']=52,['1']=53,['2']=54,['3']=55,['4']=56,['5']=57,['6']=58,['7']=59,['8']=60,['9']=61,['-']=62,['_']=63,['=']=nil} -- function decode -- decode base64 input to string function dec(data) local chars = {} local result="" for dpos=0,string.len(data)-1,4 do for char=1,4 do chars[char] = base64bytes[(string.sub(data,(dpos+char),(dpos+char)) or "=")] end result = string.format('%s%s%s%s',result,string.char(lor(lsh(chars[1],2), rsh(chars[2],4))),(chars[3] ~= nil) and string.char(lor(lsh(chars[2],4), rsh(chars[3],2))) or "",(chars[4] ~= nil) and string.char(lor(lsh(chars[3],6) % 192, (chars[4]))) or "") end return result end -- command line if not called as library if (arg ~= nil) then local func = 'enc' for n,v in ipairs(arg) do if (n > 0) then if (v == "-h") then print "base64.lua [-e] [-d] text/data" break elseif (v == "-e") then func = 'enc' elseif (v == "-d") then func = 'dec' else print(_G[func](v)) end end end else -- module('base64',package.seeall) return { dec = dec, enc = enc } end
Fetcher code
Create require_basic_auth
fetcher to verify basic authentication.
$ sudo -u haproxy cat /etc/haproxy/basic-auth.lua
-- load local base64 and sha256 libraries local base64 = require('base64') local sha256 = require('sha256') -- users map local users_map = Map.new("/etc/haproxy/users.map", Map._str) -- tokens map local tokens_map = Map.new("/etc/haproxy/tokens.map", Map._str) -- check user credentials local function check_user_credentials(username, provided_password_hash) -- deny by default local return_value = false if username ~= nil and provided_password_hash ~= nil then -- get stored password local password = users_map:lookup(username) if password ~= nil then --get current date local date = os.date("%m%d") -- get token local token = tokens_map:lookup(date) if token ~= nil then -- calculate hash local generated_hash = sha256.hash256(password .. token) -- compare hash if generated_hash == provided_password_hash then -- accept return_value = true end end end end -- return return return_value end local function require_basic_auth(txn) -- require basic auth by default local return_value=true -- get headers local headers = txn.http:req_get_headers() -- extract authorization header local authorization_header = nil for key in pairs(headers) do if key == "authorization" then if headers['authorization'][0] ~= nil then authorization_header = headers['authorization'][0] break end end end -- parse authorization header if authorization_header ~= nil then -- check authorization header format local authorization_header_format_check = false if string.find(authorization_header, "Basic (%w+)") ~= nil then authorization_header_format_check = true end -- parse authorization header if authorization_header_format_check == true then encoded_authorization_string = string.match(authorization_header, "Basic (%w+)") if encoded_authorization_string ~= nil then decoded_authorization_string = base64.dec(encoded_authorization_string) -- check authorization header for username and password local authorization_header_credentials_check = false if string.find(decoded_authorization_string, "(%w+):(%w+)") ~= nil then authorization_header_credentials_check = true end -- parse authorization header for username and password if authorization_header_credentials_check == true then username, password = string.match(decoded_authorization_string, "(%w+):(%w+)") if username ~= nil and password ~= nil then -- check username and password local credentials_check = false credentials_check = check_user_credentials(username, password) if credentials_check == true then -- user credentials verified, so do not require basic auth return_value=false end end end end end end -- log denied if return_value == true then core.Alert(string.format("[BasicAuth] Denied access for user %s [%s] to %s [%s %s %s]", username, txn.sf:src(), txn.sf:base(), txn.sf:fe_name(), txn.sf:dst(), txn.sf:dst_port() ) ) end -- return return return_value end -- register core.register_fetches('require_basic_auth', require_basic_auth)
This code is well-documented. You do not need to use tokens, as you can simply stick with the current DateTime or call an external service. The logic is up to you.
HAProxy configuration
Sample HAProxy configuration.
$ sudo -u haproxy cat /etc/haproxy/haproxy.cf
global log stdout format raw local0 info lua-load /etc/haproxy/basic-auth.lua defaults log global mode http option httplog timeout connect 5s timeout client 50s timeout server 50s frontend web-frontend bind 0.0.0.0:80 mode http acl is-dynamic hdr(host) -i dynamic.example.com http-request auth realm dynamic if { lua.require_basic_auth -m bool } is-dynamic use_backend backend-dynamic if is-dynamic backend backend-dynamic mode http http-request deny deny_status 200
Usage
Today’s date.
$ date
Sun Aug 23 15:17:18 CEST 2020
Generate current password hash. The user is milosz
, password notsosecretpassword
and token Rogers's
.
$ echo -n "notsosecretpasswordRogers's" | sha256sum - | cut -f1 -d " "
eb1fae17d5c3e33aeded8d3b4540cb9c2abbbaab8b4961a62a85fd0ca2bb60d3 -
Provide correct password hash.
$ curl http://dynamic.example.com/blog -I -s -u milosz:eb1fae17d5c3e33aeded8d3b4540cb9c2abbbaab8b4961a62a85fd0ca2bb60d3
HTTP/1.1 200 OK content-length: 58 cache-control: no-cache content-type: text/html connection: close
Provide incorrect password hash.
$ curl http://dynamic.example.com/blog -Is -u milosz:incorrectpassword
HTTP/1.1 401 Unauthorized content-length: 112 cache-control: no-cache content-type: text/html www-authenticate: Basic realm="dynamic" connection: close
There will be a log event for denied access.
sie 23 15:44:24 desktop haproxy[596917]: [alert] 235/154424 (596917) : [BasicAuth] Denied access for user milosz [127.0.0.1] to dynamic.example.com/blog [web-frontend 127.0.0.1 80]