diff --git a/Gopkg.lock b/Gopkg.lock index 2e864014c..e2d68289f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1612,7 +1612,7 @@ [[projects]] branch = "master" - digest = "1:86f14aadf288fe3ad8ac060bcb2b5083cec3829dd883803486ec834d031060c9" + digest = "1:d7ace03de79a9cec30e7a55cc16160602760470c5fe031b780dc2d84234d7f5a" name = "github.com/vulcand/oxy" packages = [ "buffer", @@ -1625,7 +1625,7 @@ "utils", ] pruneopts = "NUT" - revision = "0d102f45103cf49a95b5c6e810e092973cbcb68c" + revision = "3d629cff40b7040e0519628e7774ed11a95d9aff" [[projects]] digest = "1:ca6bac407fedc14fbeeba861dd33a821ba3a1624c10126ec6003b0a28d4139c5" diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index b75b4395b..b3fc7c03d 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -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`). +!!! note "Secure & HTTPOnly flags" + + By default, the affinity cookie is created without those flags. One however can change that through configuration. + ??? example "Adding Stickiness" ```toml [http.services] [http.services.my-service] [http.services.my-service.LoadBalancer.stickiness] + secureCookie = true + httpOnlyCookie = true ``` ??? 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.LoadBalancer.stickiness] cookieName = "my_stickiness_cookie_name" + secureCookie = true + httpOnlyCookie = true ``` #### Health Check diff --git a/pkg/config/dyn_config.go b/pkg/config/dyn_config.go index 8cf078a57..23d0b601b 100644 --- a/pkg/config/dyn_config.go +++ b/pkg/config/dyn_config.go @@ -97,7 +97,9 @@ type ResponseForwarding struct { // Stickiness holds the stickiness configuration. 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. diff --git a/pkg/provider/label/parser_test.go b/pkg/provider/label/parser_test.go index 7493a7044..5d03b2a14 100644 --- a/pkg/provider/label/parser_test.go +++ b/pkg/provider/label/parser_test.go @@ -143,6 +143,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service0.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.server.port": "8080", "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.name1": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "foobar", @@ -505,7 +506,9 @@ func TestDecodeConfiguration(t *testing.T) { "Service0": { LoadBalancer: &config.LoadBalancerService{ Stickiness: &config.Stickiness{ - CookieName: "foobar", + CookieName: "foobar", + SecureCookie: true, + HTTPOnlyCookie: false, }, Servers: []config.Server{ { @@ -897,7 +900,8 @@ func TestEncodeConfiguration(t *testing.T) { "Service0": { LoadBalancer: &config.LoadBalancerService{ Stickiness: &config.Stickiness{ - CookieName: "foobar", + CookieName: "foobar", + HTTPOnlyCookie: true, }, 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.Scheme": "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.name1": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar", diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index ad5cfceea..a92047bdd 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -192,7 +192,8 @@ func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, servi var cookieName string if stickiness := service.Stickiness; stickiness != nil { 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) } diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index c2064e62a..2243cb24a 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "github.com/containous/traefik/pkg/config" @@ -103,8 +104,10 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { defer serverPassHostFalse.Close() type ExpectedResult struct { - StatusCode int - XFrom string + StatusCode int + XFrom string + SecureCookie bool + HTTPOnlyCookie bool } 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", serviceName: "test", @@ -249,8 +272,11 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { assert.Equal(t, expected.StatusCode, recorder.Code) assert.Equal(t, expected.XFrom, recorder.Header().Get("X-From")) - if len(recorder.Header().Get("Set-Cookie")) > 0 { - req.Header.Set("Cookie", recorder.Header().Get("Set-Cookie")) + cookieHeader := 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")) } } }) diff --git a/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go b/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go index 123fbdfad..c9013b6b5 100644 --- a/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go +++ b/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go @@ -8,6 +8,13 @@ import ( // StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity type StickySession struct { 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 @@ -15,6 +22,12 @@ func NewStickySession(cookieName string) *StickySession { 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. func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.URL, bool, error) { 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 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) }