Categories
SysOps

How to enforce dynamically generated passwords for basic authentication

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]