Hi,
I tried to implement a log field filter, that filters the username name out of an Authorization header and logs it. The use case is, that a reverse proxied application does the basic authentication itself, but I still want to see username in the access log. Caddy sadly only fills the “user_id” field in the structured log, if it has done the authentication itself. I implemented the log filter as follows: (leaving out some of the not so interesting parts)
type BasicAuthFilter struct{}
// CaddyModule returns the Caddy module information.
func (BasicAuthFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.encoders.filter.basic_auth_user",
New: func() caddy.Module { return new(BasicAuthFilter) },
}
}
func (f *BasicAuthFilter) Filter(in zapcore.Field) zapcore.Field {
authHeader, ok := in.Interface.(caddyhttp.LoggableStringArray)
if !ok {
return in
}
fakeReq := &http.Request{Header: http.Header{"Authorization": authHeader}}
userID, _, ok := fakeReq.BasicAuth()
if ok {
authHeader[0] = userID
}
return in
}
and in the Caddyfile
http://:80 {
log {
output file access.log {
roll_size 20MiB
roll_keep 5
}
format filter {
wrap formatted "{request>host} {request>remote_addr} - {request>headers>Authorization>[0]} [{ts}] \"{request>method} {request>uri} {request>proto}\" {request>tls>version} {status} {size} \"{request>headers>User-Agent>[0]}\" \"{request>headers>X-Forwarded-For>[0]}\" {duration}" {
time_format "02/Jan/2006:15:04:05 -0700"
}
# wrap json
fields {
request>headers>Authorization basic_auth_user
}
}
}
redir /packagecontrol /packagecontrol/ permanent
handle /packagecontrol/* {
reverse_proxy /packagecontrol/* 127.0.0.1:9002 {
header_up Host {http.request.host}
header_up X-Real-IP {http.request.remote}
header_up X-Forwarded-For {http.request.remote}
header_up X-Forwarded-Port {http.request.port}
flush_interval 100ms
}
}
}
It actually logs the request fine in the access log
localhost [::1]:13079 - - [25/Sep/2021:18:39:25 +0000] "GET /packagecontrol/stats HTTP/1.1" - 401 92 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36" "-" 21.4393511
localhost [::1]:13079 - admin [25/Sep/2021:18:40:01 +0000] "GET /packagecontrol/stats HTTP/1.1" - 401 92 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36" "-" 28.7772383
BUT the logging extension actually changes the authorization header of the request and the reverse proxied application just gets the filtered out username instead of “Basic <Base64-string>”
If I use wrap json
, the output is the following:
{
"level": "error",
"ts": 1632595260.4081564,
"logger": "http.log.access.log0",
"msg": "handled request",
"request": {
"remote_addr": "[::1]:4983",
"proto": "HTTP/1.1",
"method": "GET",
"host": "localhost",
"uri": "/packagecontrol/stats",
"headers": {
"User-Agent": [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36"
],
"Accept-Encoding": [
"gzip, deflate, br"
],
"Connection": [
"keep-alive"
],
"Sec-Ch-Ua-Platform": [
"\"Windows\""
],
"Sec-Fetch-Site": [
"none"
],
"Authorization": [
"admin"
],
"Sec-Ch-Ua-Mobile": [
"?0"
],
"Sec-Fetch-User": [
"?1"
],
"Sec-Fetch-Dest": [
"document"
],
"Accept-Language": [
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"
],
"Upgrade-Insecure-Requests": [
"1"
],
"Dnt": [
"1"
],
"Accept": [
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
],
"Sec-Fetch-Mode": [
"navigate"
],
"Cache-Control": [
"max-age=0"
],
"Sec-Ch-Ua": [
"\"Chromium\";v=\"94\", \"Google Chrome\";v=\"94\", \";Not A Brand\";v=\"99\""
]
}
},
"common_log": "::1 - - [25/Sep/2021:20:41:00 +0200] \"GET /packagecontrol/stats HTTP/1.1\" 401 92",
"user_id": "",
"duration": 28.3982782,
"size": 92,
"status": 401,
"resp_headers": {
"Content-Type": [
"text/plain; charset=utf-8"
],
"Www-Authenticate": [
"Basic realm=\"Login Required\""
],
"Date": [
"Sat, 25 Sep 2021 18:41:00 GMT"
],
"Content-Length": [
"92"
],
"Server": [
"Caddy"
]
}
}
I have a few questions:
- Am I doing something wrong?
- Why can the logging filter change the request? Logging happens after response is send to the client, right?
- I know logging is zero-allocation, but doesn’t it seem dangerous if a logging module can interfere with actual request? (I guess the request header-values (which are
[]string
) just get passed through to the logger) - Is there a way to add new fields to the structured log from third-party caddy module?