Repository: traefik/traefik (language: go)

Issue:
K8s ingress samesite broken
### 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?

1. set `"traefik.ingress.kubernetes.io/service.sticky.cookie.sameSite"  = "strict"` on service
2. delete cookie in browser
3. refresh page
4. get cookie with no samesite setting and browser warning that samesite setting is missing
Test with "none", or "lax" or "whyyyyyyy?" does nothing, and no error.

### What did you see instead?

![image](https://github.com/traefik/traefik/assets/1201165/6ba53483-0888-4197-834e-08f37edb293d)
![image](https://github.com/traefik/traefik/assets/1201165/087ee154-56ce-414f-bb1a-aa69aa25cdf8)


### What version of Traefik are you using?

3.0.0-beta3

### What is your environment & configuration?

```yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
    traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true"
    traefik.ingress.kubernetes.io/service.sticky.cookie.samesite: strict
    traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true"
  name: whoami
  namespace: whoami
...
```



### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:
Hey @davhdavh,

Thanks for the bug report.
Could you provide a minimal reproducible case (for instance, full Kubernetes manifest to reproduce the issue)?

In the remaining time, if any community member can help us find verified steps to reproduce, we would love the help.

Any service with ingress with do (I didnt check the traafik crd, since we use external-dns and cert-manager, and they dont play well together)
See the cookie sent using this config
```
    traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
    traefik.ingress.kubernetes.io/service.sticky.cookie.samesite: strict
```
vs this config
```
    traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
    traefik.ingress.kubernetes.io/service.sticky.cookie.samesite: none
```
vs this config
```
    traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
```
They are all the same. They should NOT be the same, but have different samesite settings.
I did take a brief look at the code, and it seems the UI does not have the feature to show the setting to begin with, and I couldn't see if the unittests for k8s ingress properly covered this case.
@nmengin  Here is a shell script to reproduce this bug. It runs on an amd64 Linux system and requires the following dependencies: Docker, tar, wget, and curl commands.

```shell
#!/bin/bash
set -e

wget -O traefik2.10.3.tar.gz https://github.com/traefik/traefik/releases/download/v2.10.3/traefik_v2.10.3_linux_amd64.tar.gz
tar -xf traefik2.10.3.tar.gz  traefik -O > traefik2.10.3 && chmod +x traefik2.10.3

wget -O traefik3.0.0-beta3.tar.gz https://github.com/traefik/traefik/releases/download/v3.0.0-beta3/traefik_v3.0.0-beta3_linux_amd64.tar.gz
tar -xf traefik3.0.0-beta3.tar.gz  traefik -O > traefik3.0.0-beta3 && chmod +x traefik3.0.0-beta3

cat > traefik-conf.yaml << EOF
http:
  services:
    whoami:
      loadBalancer:
        servers:
        - url: "http://localhost:8888/"
        sticky:
          cookie:
            secure: false
            httpOnly: true
            samesite: strict
  routers:
    Router-1:
      entryPoints:
      - web
      rule: "Host(\`whoami.com\`)"
      service: "whoami"
EOF

# cleanup
function cleanup {
    echo "------ cleanup docker container ------"
    docker stop whoami

    echo "------ cleanup traefik server ------"
    kill -TERM $subp1 && wait $subp1
    kill -TERM $subp2 && wait $subp2
}

echo "------ run whoami in container ------"
docker run -d --rm -p 8888:80 --name whoami traefik/whoami

echo "------ startup traefik server ------"
echo "start traefik v2.10.3 on 8089"
./traefik2.10.3 --providers.file.filename ./traefik-conf.yaml --entryPoints.web.address=:8089 > /dev/null &
subp1=$!

echo "start traefik v3.0.0-beta3 on 8090"
./traefik3.0.0-beta3 --providers.file.filename ./traefik-conf.yaml --entryPoints.web.address=:8090 > /dev/null &
subp2=$!

trap cleanup EXIT

# wait the traefik server ready
sleep 3

echo "------ CURL ------"
echo "curl to traefik v2.10.3"
curl -v http://localhost:8089 -H "Host: whoami.com" 2>&1 | grep Set-Cookie

echo "curl to traefik v3.0.0-beta3"
curl -v http://localhost:8090 -H "Host: whoami.com" 2>&1 | grep Set-Cookie
```

After executing the shell script, we can observe the differences in behavior between v2.10.3 and v3.0.0-beta3.

```shell
curl to traefik v2.10.3
< Set-Cookie: _77b1f=75e9ab6de5c8338f; Path=/; HttpOnly; SameSite=Strict

curl to traefik v3.0.0-beta3
< Set-Cookie: _77b1f=75e9ab6de5c8338f; Path=/; HttpOnly
```

I think the difference is caused by PR #9431, which changed the load-balancer implementation from oxy/roundrobin to traefik/wrr.

Fix patch:
diff --git a/pkg/server/service/loadbalancer/wrr/wrr.go b/pkg/server/service/loadbalancer/wrr/wrr.go
index 7442fa3a19..f3a38b4c98 100644
--- a/pkg/server/service/loadbalancer/wrr/wrr.go
+++ b/pkg/server/service/loadbalancer/wrr/wrr.go
@@ -22,6 +22,20 @@ type stickyCookie struct {
 	name     string
 	secure   bool
 	httpOnly bool
+	sameSite string
+}
+
+func convertSameSite(sameSite string) http.SameSite {
+	switch sameSite {
+	case "none":
+		return http.SameSiteNoneMode
+	case "lax":
+		return http.SameSiteLaxMode
+	case "strict":
+		return http.SameSiteStrictMode
+	default:
+		return http.SameSiteDefaultMode
+	}
 }
 
 // Balancer is a WeightedRoundRobin load balancer based on Earliest Deadline First (EDF).
@@ -57,6 +71,7 @@ func New(sticky *dynamic.Sticky, wantHealthCheck bool) *Balancer {
 			name:     sticky.Cookie.Name,
 			secure:   sticky.Cookie.Secure,
 			httpOnly: sticky.Cookie.HTTPOnly,
+			sameSite: sticky.Cookie.SameSite,
 		}
 	}
 	return balancer
@@ -214,7 +229,14 @@ func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	}
 
 	if b.stickyCookie != nil {
-		cookie := &http.Cookie{Name: b.stickyCookie.name, Value: server.name, Path: "/", HttpOnly: b.stickyCookie.httpOnly, Secure: b.stickyCookie.secure}
+		cookie := &http.Cookie{
+			Name:     b.stickyCookie.name,
+			Value:    server.name,
+			Path:     "/",
+			HttpOnly: b.stickyCookie.httpOnly,
+			Secure:   b.stickyCookie.secure,
+			SameSite: convertSameSite(b.stickyCookie.sameSite),
+		}
 		http.SetCookie(w, cookie)
 	}
 


Test patch:
diff --git a/pkg/server/service/loadbalancer/wrr/wrr_test.go b/pkg/server/service/loadbalancer/wrr/wrr_test.go
index e0ca68c049..b905e55dc4 100644
--- a/pkg/server/service/loadbalancer/wrr/wrr_test.go
+++ b/pkg/server/service/loadbalancer/wrr/wrr_test.go
@@ -220,7 +220,12 @@ func TestBalancerAllServersZeroWeight(t *testing.T) {
 
 func TestSticky(t *testing.T) {
 	balancer := New(&dynamic.Sticky{
-		Cookie: &dynamic.Cookie{Name: "test"},
+		Cookie: &dynamic.Cookie{
+			Name:     "test",
+			Secure:   true,
+			HTTPOnly: true,
+			SameSite: "none",
+		},
 	}, false)
 
 	balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -233,7 +238,11 @@ func TestSticky(t *testing.T) {
 		rw.WriteHeader(http.StatusOK)
 	}), Int(2))
 
-	recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
+	recorder := &responseRecorder{
+		ResponseRecorder: httptest.NewRecorder(),
+		save:             map[string]int{},
+		cookies:          make(map[string]*http.Cookie),
+	}
 
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
 	for i := 0; i < 3; i++ {
@@ -247,6 +256,9 @@ func TestSticky(t *testing.T) {
 
 	assert.Equal(t, 0, recorder.save["first"])
 	assert.Equal(t, 3, recorder.save["second"])
+	assert.Equal(t, true, recorder.cookies["test"].HttpOnly)
+	assert.Equal(t, true, recorder.cookies["test"].Secure)
+	assert.Equal(t, http.SameSiteNoneMode, recorder.cookies["test"].SameSite)
 }
 
 // TestBalancerBias makes sure that the WRR algorithm spreads elements evenly right from the start,
@@ -282,11 +294,15 @@ type responseRecorder struct {
 	save     map[string]int
 	sequence []string
 	status   []int
+	cookies  map[string]*http.Cookie
 }
 
 func (r *responseRecorder) WriteHeader(statusCode int) {
 	r.save[r.Header().Get("server")]++
 	r.sequence = append(r.sequence, r.Header().Get("server"))
 	r.status = append(r.status, statusCode)
+	for _, cookie := range r.Result().Cookies() {
+		r.cookies[cookie.Name] = cookie
+	}
 	r.ResponseRecorder.WriteHeader(statusCode)
 }