Repository: traefik/traefik (language: go)

Issue:
Configuration Access Log Header Fields via Environment variables not working 
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

Hello,
I'm trying to limit the display of my access logs, but I have the configuration with environment variable does not see the TRAEFIK_ACCESSLOG_FIELDS_NAMES_*
It's the same if I put in keep by default and I drop the ones I don't want.

I found on the community forum someone who has the same problem but without answers : https://community.traefik.io/t/access-log-header-limiting-fields-via-environment-variables-not-working/2412

### What did you see instead?

``` json
{
  "level": "info",
  "msg": "",
  "request_User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0",
  "time": "2023-03-01T14:50:32Z"
}
```
instead of
``` json
{
  "ClientHost": "172.19.0.1",
  "ClientPort": "52186",
  "level": "info",
  "msg": "",
  "request_User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0",
  "time": "2023-03-01T14:50:32Z"
}
```

### What version of Traefik are you using?

Version:      2.9.8
Codename:     banon
Go version:   go1.19.6
Built:        2023-02-15T15:23:25Z
OS/Arch:      linux/amd64

But I tried too with 2.8, 2.7, 2.6, 2.5

### What is your environment & configuration?

```yaml
version: "3.8"

services:
  traefik:
    image: traefik:2.9
    container_name: traefik
    security_opt:
      - no-new-privileges=true
    ports:
      - 80:80
      - 443:443/tcp
      - 443:443/udp
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs:/etc/traefik/certs
      - ./config:/etc/traefik/conf/user
      - ./log:/var/log/traefik
    environment:
      - TRAEFIK_PING=true
      - TRAEFIK_API=true
      - TRAEFIK_API_INSECURE=false
      - TRAEFIK_API_DASHBOARD=true
      - TRAEFIK_API_DEBUG=false
      - TRAEFIK_EXPERIMENTAL_HTTP3=true
      - TRAEFIK_LOG=true
      - TRAEFIK_LOG_FILEPATH=/var/log/traefik/traefik.log
      - TRAEFIK_LOG_FORMAT=json

      - TRAEFIK_ACCESSLOG=true
      - TRAEFIK_ACCESSLOG_FILEPATH=/var/log/traefik/access.log
      - TRAEFIK_ACCESSLOG_FORMAT=json
      - TRAEFIK_ACCESSLOG_FIELDS_DEFAULTMODE=drop
      - TRAEFIK_ACCESSLOG_FIELDS_HEADERS_DEFAULTMODE=drop

      - TRAEFIK_ACCESSLOG_FIELDS_NAMES_ClientHost=keep
      - TRAEFIK_ACCESSLOG_FIELDS_NAMES_ClientPort=keep
      - TRAEFIK_ACCESSLOG_FIELDS_HEADERS_NAMES_User-Agent=keep

      - TRAEFIK_ENTRYPOINTS_http_ADDRESS=:80
      - TRAEFIK_ENTRYPOINTS_http_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME=https
      - TRAEFIK_ENTRYPOINTS_http_HTTP_REDIRECTIONS_ENTRYPOINT_TO=https
      - TRAEFIK_ENTRYPOINTS_https_ADDRESS=:443
      - TRAEFIK_ENTRYPOINTS_https_HTTP3=true
      - TRAEFIK_ENTRYPOINTS_https_HTTP_TLS=true
      - TRAEFIK_PROVIDERS_FILE_DIRECTORY=/etc/traefik/conf
      - TRAEFIK_PROVIDERS_FILE_WATCH=true
      - TRAEFIK_PROVIDERS_DOCKER_ENDPOINT=unix:///var/run/docker.sock
      - TRAEFIK_PROVIDERS_DOCKER_WATCH=true
      - TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
      - TRAEFIK_PROVIDERS_DOCKER_NETWORK=proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`traefik.local`)
      - traefik.http.routers.traefik.entrypoints=https
      - traefik.http.routers.traefik.service=api@internal
    restart: unless-stopped

networks:
  default:
    name: proxy
```

### If applicable, please paste the log output in DEBUG level

```
kuppit@02-1022:~/Projects/traefik$ docker compose up -d && sleep 1 && docker compose logs && tail -f log/traefik.log 
[+] Running 1/1
 ⠿ Container traefik  Started                                                                                                                                     0.9s
traefik  | time="2023-03-01T14:50:20Z" level=info msg="Configuration loaded from environment variables."
traefik  | 2023/03/01 14:50:20 packet_handler_map.go:125: failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size for details.
{"entryPointName":"http","level":"error","msg":"accept tcp [::]:80: use of closed network connection","time":"2023-03-01T14:50:12Z"}
{"entryPointName":"traefik","level":"error","msg":"close tcp [::]:8080: use of closed network connection","time":"2023-03-01T14:50:12Z"}
{"entryPointName":"http","level":"error","msg":"close tcp [::]:80: use of closed network connection","time":"2023-03-01T14:50:12Z"}
{"entryPointName":"https","level":"error","msg":"close tcp [::]:443: use of closed network connection","time":"2023-03-01T14:50:12Z"}
{"entryPointName":"http","level":"error","msg":"accept tcp [::]:80: use of closed network connection","time":"2023-03-01T14:50:19Z"}
{"entryPointName":"traefik","level":"error","msg":"accept tcp [::]:8080: use of closed network connection","time":"2023-03-01T14:50:19Z"}
{"entryPointName":"https","level":"error","msg":"accept tcp [::]:443: use of closed network connection","time":"2023-03-01T14:50:19Z"}
{"entryPointName":"traefik","level":"error","msg":"close tcp [::]:8080: use of closed network connection","time":"2023-03-01T14:50:19Z"}
{"entryPointName":"http","level":"error","msg":"close tcp [::]:80: use of closed network connection","time":"2023-03-01T14:50:19Z"}
{"entryPointName":"https","level":"error","msg":"close tcp [::]:443: use of closed network connection","time":"2023-03-01T14:50:19Z"}
```


Discussion / hints:
Hello @Kuppit,

Thanks for your interest in Traefik!

We did reproduce the bug.
The static configuration is indeed taken into account, but the values are parsed in lowercase with ENV variables, and the access log filter is case-sensitive, so it can't match.

We are marking this issue as bug confirmed, but as a workaround, we can suggest using CLI arg to configure Traefik with docker-compose, which doesn't suffer from the same bug.
I can only use the environment configuration because I need to override the static configuration of my base docker image from traefik. And I read it is not possible to merge 2 configuration file and with CLI configuration I will have to redefine the whole config each time

Example: On 90% of my servers I redirect HTTP to HTTPS, and when I have to deactivate that I have to overload the static configuration
```yaml
TRAEFIK_ENTRYPOINTS_http_ADDRESS=:80
TRAEFIK_ENTRYPOINTS_http_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME=https
TRAEFIK_ENTRYPOINTS_http_HTTP_REDIRECTIONS_ENTRYPOINT_TO=https
TRAEFIK_ENTRYPOINTS_https_ADDRESS=:443
```

Hello @Kuppit ,

To restate what @rtribotte said, but maybe a bit differently: since the config file has the priority over the CLI args, what you could do is:

1) Copy/translate all of your config by envs into a config file. And change in that config file the few bits that you need to be different in 10% of cases.
2) You translate all of your env variables in your compose as CLI arguments (remove any configuration by env var).
3) When you're in the 10% case, your compose file mounts the volume where the config file, and in that case the config file will be the one that completely takes over the configuration.
4) When you're in the 90% case, your compose file does not mount the volume where the config file is, and in that case the config is fully defined by CLI only.




Fix patch:
diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go
index cf729c6def..ed85a8888c 100644
--- a/pkg/middlewares/accesslog/logger.go
+++ b/pkg/middlewares/accesslog/logger.go
@@ -105,15 +105,28 @@ func NewHandler(config *types.AccessLog) (*Handler, error) {
 		Level:     logrus.InfoLevel,
 	}
 
-	// Transform headers names in config to a canonical form, to be used as is without further transformations.
-	if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 {
-		fields := map[string]string{}
+	// Transform header names to a canonical form, to be used as is without further transformations,
+	// and transform field names to lower case, to enable case-insensitive lookup.
+	if config.Fields != nil {
+		if len(config.Fields.Names) > 0 {
+			fields := map[string]string{}
+
+			for h, v := range config.Fields.Names {
+				fields[strings.ToLower(h)] = v
+			}
 
-		for h, v := range config.Fields.Headers.Names {
-			fields[textproto.CanonicalMIMEHeaderKey(h)] = v
+			config.Fields.Names = fields
 		}
 
-		config.Fields.Headers.Names = fields
+		if config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 {
+			fields := map[string]string{}
+
+			for h, v := range config.Fields.Headers.Names {
+				fields[textproto.CanonicalMIMEHeaderKey(h)] = v
+			}
+
+			config.Fields.Headers.Names = fields
+		}
 	}
 
 	logHandler := &Handler{
@@ -332,7 +345,7 @@ func (h *Handler) logTheRoundTrip(logDataTable *LogData) {
 		fields := logrus.Fields{}
 
 		for k, v := range logDataTable.Core {
-			if h.config.Fields.Keep(k) {
+			if h.config.Fields.Keep(strings.ToLower(k)) {
 				fields[k] = v
 			}
 		}


Test patch:
diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go
index 338467293b..46563db611 100644
--- a/pkg/middlewares/accesslog/logger_test.go
+++ b/pkg/middlewares/accesslog/logger_test.go
@@ -462,6 +462,32 @@ func TestLoggerJSON(t *testing.T) {
 				RequestRefererHeader: assertString(testReferer),
 			},
 		},
+		{
+			desc: "fields and headers with unconventional letter case",
+			config: &types.AccessLog{
+				FilePath: "",
+				Format:   JSONFormat,
+				Fields: &types.AccessLogFields{
+					DefaultMode: "drop",
+					Names: map[string]string{
+						"rEqUeStHoSt": "keep",
+					},
+					Headers: &types.FieldHeaders{
+						DefaultMode: "drop",
+						Names: map[string]string{
+							"ReFeReR": "keep",
+						},
+					},
+				},
+			},
+			expected: map[string]func(t *testing.T, value interface{}){
+				RequestHost:          assertString(testHostname),
+				"level":              assertString("info"),
+				"msg":                assertString(""),
+				"time":               assertNotEmpty(),
+				RequestRefererHeader: assertString(testReferer),
+			},
+		},
 	}
 
 	for _, test := range testCases {