HttpOnly and Secure flags on the affinity cookie

This commit is contained in:
Amir Keibi 2019-06-12 15:42:06 -07:00 committed by Traefiker Bot
parent cad3704efd
commit d18edd6f77
7 changed files with 68 additions and 11 deletions

4
Gopkg.lock generated
View file

@ -1612,7 +1612,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:86f14aadf288fe3ad8ac060bcb2b5083cec3829dd883803486ec834d031060c9" digest = "1:d7ace03de79a9cec30e7a55cc16160602760470c5fe031b780dc2d84234d7f5a"
name = "github.com/vulcand/oxy" name = "github.com/vulcand/oxy"
packages = [ packages = [
"buffer", "buffer",
@ -1625,7 +1625,7 @@
"utils", "utils",
] ]
pruneopts = "NUT" pruneopts = "NUT"
revision = "0d102f45103cf49a95b5c6e810e092973cbcb68c" revision = "3d629cff40b7040e0519628e7774ed11a95d9aff"
[[projects]] [[projects]]
digest = "1:ca6bac407fedc14fbeeba861dd33a821ba3a1624c10126ec6003b0a28d4139c5" digest = "1:ca6bac407fedc14fbeeba861dd33a821ba3a1624c10126ec6003b0a28d4139c5"

View file

@ -103,12 +103,18 @@ On subsequent requests, the client is forwarded to the same server.
The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`). The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`).
!!! note "Secure & HTTPOnly flags"
By default, the affinity cookie is created without those flags. One however can change that through configuration.
??? example "Adding Stickiness" ??? example "Adding Stickiness"
```toml ```toml
[http.services] [http.services]
[http.services.my-service] [http.services.my-service]
[http.services.my-service.LoadBalancer.stickiness] [http.services.my-service.LoadBalancer.stickiness]
secureCookie = true
httpOnlyCookie = true
``` ```
??? example "Adding Stickiness with a Custom Cookie Name" ??? example "Adding Stickiness with a Custom Cookie Name"
@ -118,6 +124,8 @@ On subsequent requests, the client is forwarded to the same server.
[http.services.my-service] [http.services.my-service]
[http.services.my-service.LoadBalancer.stickiness] [http.services.my-service.LoadBalancer.stickiness]
cookieName = "my_stickiness_cookie_name" cookieName = "my_stickiness_cookie_name"
secureCookie = true
httpOnlyCookie = true
``` ```
#### Health Check #### Health Check

View file

@ -98,6 +98,8 @@ type ResponseForwarding struct {
// Stickiness holds the stickiness configuration. // Stickiness holds the stickiness configuration.
type Stickiness struct { type Stickiness struct {
CookieName string `json:"cookieName,omitempty" toml:",omitempty"` CookieName string `json:"cookieName,omitempty" toml:",omitempty"`
SecureCookie bool `json:"secureCookie,omitempty" toml:",omitempty"`
HTTPOnlyCookie bool `json:"httpOnlyCookie,omitempty" toml:",omitempty"`
} }
// Server holds the server configuration. // Server holds the server configuration.

View file

@ -143,6 +143,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.services.Service0.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.server.scheme": "foobar",
"traefik.http.services.Service0.loadbalancer.server.port": "8080", "traefik.http.services.Service0.loadbalancer.server.port": "8080",
"traefik.http.services.Service0.loadbalancer.stickiness.cookiename": "foobar", "traefik.http.services.Service0.loadbalancer.stickiness.cookiename": "foobar",
"traefik.http.services.Service0.loadbalancer.stickiness.securecookie": "true",
"traefik.http.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "foobar",
@ -506,6 +507,8 @@ func TestDecodeConfiguration(t *testing.T) {
LoadBalancer: &config.LoadBalancerService{ LoadBalancer: &config.LoadBalancerService{
Stickiness: &config.Stickiness{ Stickiness: &config.Stickiness{
CookieName: "foobar", CookieName: "foobar",
SecureCookie: true,
HTTPOnlyCookie: false,
}, },
Servers: []config.Server{ Servers: []config.Server{
{ {
@ -898,6 +901,7 @@ func TestEncodeConfiguration(t *testing.T) {
LoadBalancer: &config.LoadBalancerService{ LoadBalancer: &config.LoadBalancerService{
Stickiness: &config.Stickiness{ Stickiness: &config.Stickiness{
CookieName: "foobar", CookieName: "foobar",
HTTPOnlyCookie: true,
}, },
Servers: []config.Server{ Servers: []config.Server{
{ {
@ -1086,6 +1090,8 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Services.Service0.LoadBalancer.server.Port": "8080", "traefik.HTTP.Services.Service0.LoadBalancer.server.Port": "8080",
"traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.CookieName": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.CookieName": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.HTTPOnlyCookie": "true",
"traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.SecureCookie": "false",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar",

View file

@ -192,7 +192,8 @@ func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, servi
var cookieName string var cookieName string
if stickiness := service.Stickiness; stickiness != nil { if stickiness := service.Stickiness; stickiness != nil {
cookieName = cookie.GetName(stickiness.CookieName, serviceName) cookieName = cookie.GetName(stickiness.CookieName, serviceName)
options = append(options, roundrobin.EnableStickySession(roundrobin.NewStickySession(cookieName))) opts := roundrobin.CookieOptions{HTTPOnly: stickiness.HTTPOnlyCookie, Secure: stickiness.SecureCookie}
options = append(options, roundrobin.EnableStickySession(roundrobin.NewStickySessionWithOptions(cookieName, opts)))
logger.Debugf("Sticky session cookie name: %v", cookieName) logger.Debugf("Sticky session cookie name: %v", cookieName)
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
@ -105,6 +106,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
type ExpectedResult struct { type ExpectedResult struct {
StatusCode int StatusCode int
XFrom string XFrom string
SecureCookie bool
HTTPOnlyCookie bool
} }
testCases := []struct { testCases := []struct {
@ -192,6 +195,26 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
}, },
}, },
}, },
{
desc: "Sticky Cookie's options set correctly",
serviceName: "test",
service: &config.LoadBalancerService{
Stickiness: &config.Stickiness{HTTPOnlyCookie: true, SecureCookie: true},
Servers: []config.Server{
{
URL: server1.URL,
},
},
},
expected: []ExpectedResult{
{
StatusCode: http.StatusOK,
XFrom: "first",
SecureCookie: true,
HTTPOnlyCookie: true,
},
},
},
{ {
desc: "PassHost passes the host instead of the IP", desc: "PassHost passes the host instead of the IP",
serviceName: "test", serviceName: "test",
@ -249,8 +272,11 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
assert.Equal(t, expected.StatusCode, recorder.Code) assert.Equal(t, expected.StatusCode, recorder.Code)
assert.Equal(t, expected.XFrom, recorder.Header().Get("X-From")) assert.Equal(t, expected.XFrom, recorder.Header().Get("X-From"))
if len(recorder.Header().Get("Set-Cookie")) > 0 { cookieHeader := recorder.Header().Get("Set-Cookie")
req.Header.Set("Cookie", recorder.Header().Get("Set-Cookie")) if len(cookieHeader) > 0 {
req.Header.Set("Cookie", cookieHeader)
assert.Equal(t, expected.SecureCookie, strings.Contains(cookieHeader, "Secure"))
assert.Equal(t, expected.HTTPOnlyCookie, strings.Contains(cookieHeader, "HttpOnly"))
} }
} }
}) })

View file

@ -8,6 +8,13 @@ import (
// StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity // StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity
type StickySession struct { type StickySession struct {
cookieName string cookieName string
options CookieOptions
}
// CookieOptions has all the options one would like to set on the affinity cookie
type CookieOptions struct {
HTTPOnly bool
Secure bool
} }
// NewStickySession creates a new StickySession // NewStickySession creates a new StickySession
@ -15,6 +22,12 @@ func NewStickySession(cookieName string) *StickySession {
return &StickySession{cookieName: cookieName} return &StickySession{cookieName: cookieName}
} }
// NewStickySessionWithOptions creates a new StickySession whilst allowing for options to
// shape its affinity cookie such as "httpOnly" or "secure"
func NewStickySessionWithOptions(cookieName string, options CookieOptions) *StickySession {
return &StickySession{cookieName: cookieName, options: options}
}
// GetBackend returns the backend URL stored in the sticky cookie, iff the backend is still in the valid list of servers. // GetBackend returns the backend URL stored in the sticky cookie, iff the backend is still in the valid list of servers.
func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.URL, bool, error) { func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.URL, bool, error) {
cookie, err := req.Cookie(s.cookieName) cookie, err := req.Cookie(s.cookieName)
@ -39,7 +52,8 @@ func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.
// StickBackend creates and sets the cookie // StickBackend creates and sets the cookie
func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) { func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) {
cookie := &http.Cookie{Name: s.cookieName, Value: backend.String(), Path: "/"} opt := s.options
cookie := &http.Cookie{Name: s.cookieName, Value: backend.String(), Path: "/", HttpOnly: opt.HTTPOnly, Secure: opt.Secure}
http.SetCookie(*w, cookie) http.SetCookie(*w, cookie)
} }