From ffa9a1871cc829b3b32afc2f13dc16e95cdebded Mon Sep 17 00:00:00 2001 From: SaSol Date: Wed, 22 Feb 2017 15:56:02 +0100 Subject: [PATCH 01/10] USERS_PASS_ENCRYPTED env variable tells proxy if passwords provided are encrypted, simmilary to usersPassEncrypted reconfigure parameter. usersPath parameter will tell proxy which file to use for users for given service. --- actions/reconfigure.go | 7 ++- actions/reconfigure_test.go | 26 ++++++++++ integration_tests/integration_swarm_test.go | 26 ++++++++++ proxy/ha_proxy.go | 53 ++++++++++----------- proxy/ha_proxy_test.go | 32 +++++++++++++ proxy/types.go | 1 + proxy/util.go | 13 +++++ scripts/local-ci.sh | 16 +++++++ server.go | 31 ++++++++++-- server_test.go | 48 +++++++++++++++++++ test_configs/users.txt | 2 + 11 files changed, 219 insertions(+), 36 deletions(-) create mode 100755 scripts/local-ci.sh create mode 100644 test_configs/users.txt diff --git a/actions/reconfigure.go b/actions/reconfigure.go index 55d8fdb3..4b534dea 100644 --- a/actions/reconfigure.go +++ b/actions/reconfigure.go @@ -9,7 +9,6 @@ import ( "html/template" "io/ioutil" "net/http" - "os" "strconv" "strings" "sync" @@ -395,7 +394,7 @@ backend %s{{$.ServiceName}}-be{{.Port}} acl {{$.ServiceName}}UsersAcl http_auth({{$.ServiceName}}Users) http-request auth realm {{$.ServiceName}}Realm if !{{$.ServiceName}}UsersAcl http-request del-header Authorization` - } else if len(os.Getenv("USERS")) > 0 { + } else if len(proxy.GetSecretOrEnvVar("USERS", "")) > 0 { tmpl += ` acl defaultUsersAcl http_auth(defaultUsers) http-request auth realm defaultRealm if !defaultUsersAcl @@ -407,8 +406,8 @@ backend %s{{$.ServiceName}}-be{{.Port}} func (m *Reconfigure) getUsersList(sr *proxy.Service) string { if len(sr.Users) > 0 { - return `userlist {{.ServiceName}}Users{{range .Users}} - user {{.Username}} insecure-password {{.Password}}{{end}} + return `{{$service := .}}userlist {{.ServiceName}}Users{{range .Users}} + user {{.Username}} {{if $service.UsersPassEncrypted}}password{{end}}{{if not $service.UsersPassEncrypted}}insecure-password{{end}} {{.Password}}{{end}} ` } diff --git a/actions/reconfigure_test.go b/actions/reconfigure_test.go index fa275a61..a6b0cbab 100644 --- a/actions/reconfigure_test.go +++ b/actions/reconfigure_test.go @@ -182,6 +182,32 @@ func (s ReconfigureTestSuite) Test_GetTemplates_AddsHttpAuth_WhenUsersIsPresent( user user-2 insecure-password pass-2 +backend myService-be + mode http + http-request add-header X-Forwarded-Proto https if { ssl_fc } + {{range $i, $e := service "myService" "any"}} + server {{$e.Node}}_{{$i}}_{{$e.Port}} {{$e.Address}}:{{$e.Port}} check + {{end}} + acl myServiceUsersAcl http_auth(myServiceUsers) + http-request auth realm myServiceRealm if !myServiceUsersAcl + http-request del-header Authorization` + + _, back, _ := s.reconfigure.GetTemplates(&s.reconfigure.Service) + + s.Equal(expected, back) +} + +func (s ReconfigureTestSuite) Test_GetTemplates_AddsHttpAuth_WhenUsersIsPresentAndPasswordsEncrypted() { + s.reconfigure.Users = []proxy.User{ + {Username: "user-1", Password: "pass-1"}, + {Username: "user-2", Password: "pass-2"}, + } + s.reconfigure.UsersPassEncrypted = true + expected := `userlist myServiceUsers + user user-1 password pass-1 + user user-2 password pass-2 + + backend myService-be mode http http-request add-header X-Forwarded-Proto https if { ssl_fc } diff --git a/integration_tests/integration_swarm_test.go b/integration_tests/integration_swarm_test.go index ad82d549..9f8baa94 100644 --- a/integration_tests/integration_swarm_test.go +++ b/integration_tests/integration_swarm_test.go @@ -148,6 +148,32 @@ func (s IntegrationSwarmTestSuite) Test_GlobalAuthentication() { s.Equal(200, resp.StatusCode, s.getProxyConf()) } +func (s IntegrationSwarmTestSuite) Test_GlobalAuthenticationWithEncryption() { + defer func() { + exec.Command("/bin/sh", "-c", `docker service update --env-rm "USERS" proxy`).Output() + s.waitForContainers(1, "proxy") + }() + _, err := exec.Command("/bin/sh", "-c", `docker service update --env-add "USERS_PASS_ENCRYPTED=true" --env-add "USERS=my-user:\$6\$AcrjVWOkQq1vWp\$t55F7Psm3Ujvp8lpqdAwrc5RxWORYBeDV6ji9KoO029ojooj4Pi.JVGwxdicB0Fuu.NSDyGaZt7skHIo3Nayq/" proxy`).Output() + s.NoError(err) + s.waitForContainers(1, "proxy") + + s.reconfigureGoDemo("") + + resp, err := s.sendHelloRequest() + + s.NoError(err) + s.Equal(401, resp.StatusCode, s.getProxyConf()) + + url := fmt.Sprintf("http://%s/demo/hello", s.hostIP) + req, err := http.NewRequest("GET", url, nil) + req.SetBasicAuth("my-user", "my-pass") + client := &http.Client{} + resp, err = client.Do(req) + + s.NoError(err) + s.Equal(200, resp.StatusCode, s.getProxyConf()) +} + func (s IntegrationSwarmTestSuite) Test_ServiceAuthentication() { defer func() { s.reconfigureGoDemo("") diff --git a/proxy/ha_proxy.go b/proxy/ha_proxy.go index f5ca2bc8..ebf56d82 100644 --- a/proxy/ha_proxy.go +++ b/proxy/ha_proxy.go @@ -174,26 +174,32 @@ func (m HaProxy) getConfigData() ConfigData { d := ConfigData{ CertsString: strings.Join(certs, " "), } - d.ConnectionMode = m.getSecretOrEnvVar("CONNECTION_MODE", "http-server-close") - d.TimeoutConnect = m.getSecretOrEnvVar("TIMEOUT_CONNECT", "5") - d.TimeoutClient = m.getSecretOrEnvVar("TIMEOUT_CLIENT", "20") - d.TimeoutServer = m.getSecretOrEnvVar("TIMEOUT_SERVER", "20") - d.TimeoutQueue = m.getSecretOrEnvVar("TIMEOUT_QUEUE", "30") - d.TimeoutTunnel = m.getSecretOrEnvVar("TIMEOUT_TUNNEL", "3600") - d.TimeoutHttpRequest = m.getSecretOrEnvVar("TIMEOUT_HTTP_REQUEST", "5") - d.TimeoutHttpKeepAlive = m.getSecretOrEnvVar("TIMEOUT_HTTP_KEEP_ALIVE", "15") - d.StatsUser = m.getSecretOrEnvVar("STATS_USER", "admin") - d.StatsPass = m.getSecretOrEnvVar("STATS_PASS", "admin") - usersString := m.getSecretOrEnvVar("USERS", "") + d.ConnectionMode = GetSecretOrEnvVar("CONNECTION_MODE", "http-server-close") + d.TimeoutConnect = GetSecretOrEnvVar("TIMEOUT_CONNECT", "5") + d.TimeoutClient = GetSecretOrEnvVar("TIMEOUT_CLIENT", "20") + d.TimeoutServer = GetSecretOrEnvVar("TIMEOUT_SERVER", "20") + d.TimeoutQueue = GetSecretOrEnvVar("TIMEOUT_QUEUE", "30") + d.TimeoutTunnel = GetSecretOrEnvVar("TIMEOUT_TUNNEL", "3600") + d.TimeoutHttpRequest = GetSecretOrEnvVar("TIMEOUT_HTTP_REQUEST", "5") + d.TimeoutHttpKeepAlive = GetSecretOrEnvVar("TIMEOUT_HTTP_KEEP_ALIVE", "15") + d.StatsUser = GetSecretOrEnvVar("STATS_USER", "admin") + d.StatsPass = GetSecretOrEnvVar("STATS_PASS", "admin") + usersString := GetSecretOrEnvVar("USERS", "") + encryptedString := GetSecretOrEnvVar("USERS_PASS_ENCRYPTED", "") if len(usersString) > 0 { d.UserList = "\nuserlist defaultUsers\n" + passwordType := "insecure-password"; + if encryptedString == "true" || encryptedString == "1" { + passwordType = "password"; + } users := strings.Split(usersString, ",") for _, user := range users { - userPass := strings.Split(user, ":") - d.UserList = fmt.Sprintf("%s user %s insecure-password %s\n", d.UserList, userPass[0], userPass[1]) + //trimming to allow new lines in file + userPass := strings.Split(strings.Trim(user, "\n\t "), ":") + d.UserList = fmt.Sprintf("%s user %s %s %s\n", d.UserList, userPass[0], passwordType, userPass[1]) } } - if strings.EqualFold(m.getSecretOrEnvVar("DEBUG", ""), "true") { + if strings.EqualFold(GetSecretOrEnvVar("DEBUG", ""), "true") { d.ExtraGlobal += ` debug` } else { @@ -202,18 +208,18 @@ func (m HaProxy) getConfigData() ConfigData { option dontlog-normal` } - defaultPortsString := m.getSecretOrEnvVar("DEFAULT_PORTS", "") + defaultPortsString := GetSecretOrEnvVar("DEFAULT_PORTS", "") defaultPorts := strings.Split(defaultPortsString, ",") for _, bindPort := range defaultPorts { formattedPort := strings.Replace(bindPort, ":ssl", d.CertsString, -1) d.DefaultBinds += fmt.Sprintf("\n bind *:%s", formattedPort) } - d.ExtraFrontend = m.getSecretOrEnvVar("EXTRA_FRONTEND", "") - extraGlobal := m.getSecretOrEnvVar("EXTRA_GLOBAL", "") + d.ExtraFrontend = GetSecretOrEnvVar("EXTRA_FRONTEND", "") + extraGlobal := GetSecretOrEnvVar("EXTRA_GLOBAL", "") if len(extraGlobal) > 0 { d.ExtraGlobal += fmt.Sprintf("\n %s", extraGlobal) } - bindPortsString := m.getSecretOrEnvVar("BIND_PORTS", "") + bindPortsString := GetSecretOrEnvVar("BIND_PORTS", "") if len(bindPortsString) > 0 { bindPorts := strings.Split(bindPortsString, ",") for _, bindPort := range bindPorts { @@ -257,16 +263,7 @@ func (m HaProxy) getConfigData() ConfigData { return d } -func (m *HaProxy) getSecretOrEnvVar(key, defaultValue string) string { - path := fmt.Sprintf("/run/secrets/dfp_%s", strings.ToLower(key)) - if content, err := readSecretsFile(path); err == nil { - return strings.TrimRight(string(content[:]), "\n") - } - if len(os.Getenv(key)) > 0 { - return os.Getenv(key) - } - return defaultValue -} + func (m *HaProxy) getFrontTemplateSNI(s Service, gen_header bool) string { tmplString := `` diff --git a/proxy/ha_proxy_test.go b/proxy/ha_proxy_test.go index 5dc64671..e4e4d6cb 100644 --- a/proxy/ha_proxy_test.go +++ b/proxy/ha_proxy_test.go @@ -691,6 +691,38 @@ frontend services`, s.Equal(expectedData, actualData) } + +func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_AddsUserListWithEncryptedPasswordsOn() { + var actualData string + usersOrig := os.Getenv("USERS") + encOrig := os.Getenv("USERS_PASS_ENCRYPTED") + defer func() { os.Setenv("USERS", usersOrig); os.Setenv("USERS_PASS_ENCRYPTED", encOrig)}() + os.Setenv("USERS", "my-user-1:my-password-1,my-user-2:my-password-2") + os.Setenv("USERS_PASS_ENCRYPTED", "true") + expectedData := fmt.Sprintf( + "%s%s", + strings.Replace( + s.TemplateContent, + "frontend services", + `userlist defaultUsers + user my-user-1 password my-password-1 + user my-user-2 password my-password-2 + +frontend services`, + -1, + ), + s.ServicesContent, + ) + writeFile = func(filename string, data []byte, perm os.FileMode) error { + actualData = string(data) + return nil + } + + NewHaProxy(s.TemplatesPath, s.ConfigsPath, map[string]bool{}).CreateConfigFromTemplates() + + s.Equal(expectedData, actualData) +} + func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ReplacesValuesWithEnvVars() { tests := []struct { envKey string diff --git a/proxy/types.go b/proxy/types.go index 118f72e6..b34f05e6 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -82,6 +82,7 @@ type Service struct { TimeoutTunnel string // A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured. Users []User + UsersPassEncrypted bool ServiceColor string ServicePort string AclCondition string diff --git a/proxy/util.go b/proxy/util.go index bc8442ee..9a82a4e9 100644 --- a/proxy/util.go +++ b/proxy/util.go @@ -4,6 +4,9 @@ import ( "io/ioutil" "log" "os/exec" + "fmt" + "strings" + "os" ) var cmdRunHa = func(cmd *exec.Cmd) error { @@ -16,3 +19,13 @@ var ReadFile = ioutil.ReadFile var logPrintf = log.Printf var readPidFile = ioutil.ReadFile var readConfigsDir = ioutil.ReadDir +var GetSecretOrEnvVar = func(key, defaultValue string) string { + path := fmt.Sprintf("/run/secrets/dfp_%s", strings.ToLower(key)) + if content, err := readSecretsFile(path); err == nil { + return strings.TrimRight(string(content[:]), "\n") + } + if len(os.Getenv(key)) > 0 { + return os.Getenv(key) + } + return defaultValue +} diff --git a/scripts/local-ci.sh b/scripts/local-ci.sh new file mode 100755 index 00000000..6e4a7583 --- /dev/null +++ b/scripts/local-ci.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e +if [[ ( -z ${DOCKER_HUB_USER} ) || ( -z ${HOST_IP} ) ]]; then + echo "set DOCKER_HUB_USER variable to your docker hub account, HOST_IP to your host Ip before running" + exit 1 +fi + +echo Running in $PWD +docker run --rm -v $PWD:/usr/src/myapp -w /usr/src/myapp -v go:/go golang:1.6 bash -c "go get -d -v -t && go test --cover ./... --run UnitTest && go build -v -o docker-flow-proxy" +docker build -t $DOCKER_HUB_USER/docker-flow-proxy . +docker-compose -f docker-compose-test.yml up -d staging-dep +docker-compose -f docker-compose-test.yml run --rm staging +docker-compose -f docker-compose-test.yml down +docker tag $DOCKER_HUB_USER/docker-flow-proxy $DOCKER_HUB_USER/docker-flow-proxy:beta +docker push $DOCKER_HUB_USER/docker-flow-proxy:beta +docker-compose -f docker-compose-test.yml run --rm staging-swarm \ No newline at end of file diff --git a/server.go b/server.go index 293cbb46..17c115d9 100644 --- a/server.go +++ b/server.go @@ -10,6 +10,8 @@ import ( "os" "strconv" "strings" + "io/ioutil" + "math/rand" ) const ( @@ -204,6 +206,14 @@ func (m *Serve) reconfigure(w http.ResponseWriter, req *http.Request) { w.Write(js) } +func appendUsersFromString(sr *proxy.Service, commaSeparatedUsers string) { + users := strings.Split(commaSeparatedUsers, ",") + for _, user := range users { + userPass := strings.Split(strings.Trim(user, "\n\t "), ":") + sr.Users = append(sr.Users, proxy.User{Username: userPass[0], Password: userPass[1]}) + } +} + func (m *Serve) getService(sd []proxy.ServiceDest, req *http.Request) proxy.Service { sr := proxy.Service{ ServiceDest: sd, @@ -240,11 +250,24 @@ func (m *Serve) getService(sd []proxy.ServiceDest, req *http.Request) proxy.Serv sr.SkipCheck = m.getBoolParam(req, "skipCheck") sr.Distribute = m.getBoolParam(req, "distribute") sr.SslVerifyNone = m.getBoolParam(req, "sslVerifyNone") + sr.UsersPassEncrypted = m.getBoolParam(req, "usersPassEncrypted") + if len(req.URL.Query().Get("users")) > 0 { - users := strings.Split(req.URL.Query().Get("users"), ",") - for _, user := range users { - userPass := strings.Split(user, ":") - sr.Users = append(sr.Users, proxy.User{Username: userPass[0], Password: userPass[1]}) + appendUsersFromString(&sr, req.URL.Query().Get("users")) + } else if len(req.URL.Query().Get("usersPath")) > 0 { + usersPath := req.URL.Query().Get("usersPath"); + if content, err := ioutil.ReadFile(usersPath); err == nil { + userContents := strings.TrimRight(string(content[:]), "\n") + appendUsersFromString(&sr, userContents) + }else { + logPrintf("For service %s it was impossible to load userFile %s due to error %s", + sr.ServiceName, usersPath, err.Error()) + //shouldn't we add some rondom user? if Users is empty the service will be unprotected, + // but obviously someone wanted it to be secured + sr.Users = append(sr.Users, proxy.User{ + Username: "dummyUser", + Password: strconv.FormatInt(rand.Int63(), 3)}, + ) } } return sr diff --git a/server_test.go b/server_test.go index bc8febd1..36401d1d 100644 --- a/server_test.go +++ b/server_test.go @@ -653,6 +653,54 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsers_WhenPresent() { s.ResponseWriter.AssertCalled(s.T(), "Write", []byte(expected)) } +func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersFromUsersFile_WhenPresent() { + users := []proxy.User{ + {Username: "user1", Password: "pass1"}, + {Username: "user2", Password: "pass2"}, + } + req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersPath=./test_configs/users.txt", nil) + expected, _ := json.Marshal(server.Response{ + Status: "OK", + ServiceName: s.ServiceName, + Service: proxy.Service{ + ReqMode: "http", + ServiceName: s.ServiceName, + ServiceColor: s.ServiceColor, + ServiceDomain: s.ServiceDomain, + OutboundHostname: s.OutboundHostname, + Users: users, + ServiceDest: []proxy.ServiceDest{s.sd}, + }, + }) + + srv := Serve{} + srv.ServeHTTP(s.ResponseWriter, req) + + s.ResponseWriter.AssertCalled(s.T(), "Write", []byte(expected)) +} + +func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersPassEncrypted_WhenPresent() { + req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersPassEncrypted=true", nil) + expected, _ := json.Marshal(server.Response{ + Status: "OK", + ServiceName: s.ServiceName, + Service: proxy.Service{ + ReqMode: "http", + ServiceName: s.ServiceName, + ServiceColor: s.ServiceColor, + ServiceDomain: s.ServiceDomain, + OutboundHostname: s.OutboundHostname, + UsersPassEncrypted: true, + ServiceDest: []proxy.ServiceDest{s.sd}, + }, + }) + + srv := Serve{} + srv.ServeHTTP(s.ResponseWriter, req) + + s.ResponseWriter.AssertCalled(s.T(), "Write", []byte(expected)) +} + func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithPorts_WhenPresent() { port := "1234" httpsPort := 4321 diff --git a/test_configs/users.txt b/test_configs/users.txt new file mode 100644 index 00000000..d26981d2 --- /dev/null +++ b/test_configs/users.txt @@ -0,0 +1,2 @@ +user1:pass1, +user2:pass2 \ No newline at end of file From c63da268e7b5b9004c02b51dd0caabb91c551efe Mon Sep 17 00:00:00 2001 From: SaSol Date: Mon, 27 Feb 2017 11:59:37 +0100 Subject: [PATCH 02/10] code fixes as suggested by @vfarcic, change usersPath to usersSecret, common prefix for usersSecret /run/secret/dfp_users_%s --- actions/reconfigure.go | 4 ++-- proxy/ha_proxy.go | 2 +- proxy/ha_proxy_test.go | 2 +- server.go | 13 ++++++++----- server_test.go | 15 ++++++++------- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/actions/reconfigure.go b/actions/reconfigure.go index d956dcda..a0fc6958 100644 --- a/actions/reconfigure.go +++ b/actions/reconfigure.go @@ -406,8 +406,8 @@ backend %s{{$.ServiceName}}-be{{.Port}} func (m *Reconfigure) getUsersList(sr *proxy.Service) string { if len(sr.Users) > 0 { - return `{{$service := .}}userlist {{.ServiceName}}Users{{range .Users}} - user {{.Username}} {{if $service.UsersPassEncrypted}}password{{end}}{{if not $service.UsersPassEncrypted}}insecure-password{{end}} {{.Password}}{{end}} + return `userlist {{.ServiceName}}Users{{range .Users}} + user {{.Username}} {{if $.UsersPassEncrypted}}password{{end}}{{if not $.UsersPassEncrypted}}insecure-password{{end}} {{.Password}}{{end}} ` } diff --git a/proxy/ha_proxy.go b/proxy/ha_proxy.go index dd8e28e3..0ab3c645 100644 --- a/proxy/ha_proxy.go +++ b/proxy/ha_proxy.go @@ -195,7 +195,7 @@ func (m HaProxy) getConfigData() ConfigData { if len(usersString) > 0 { d.UserList = "\nuserlist defaultUsers\n" passwordType := "insecure-password"; - if encryptedString == "true" || encryptedString == "1" { + if strings.EqualFold(encryptedString ,"true") || strings.EqualFold(encryptedString , "1") { passwordType = "password"; } users := strings.Split(usersString, ",") diff --git a/proxy/ha_proxy_test.go b/proxy/ha_proxy_test.go index 4274b9e1..4489eb8d 100644 --- a/proxy/ha_proxy_test.go +++ b/proxy/ha_proxy_test.go @@ -819,7 +819,7 @@ frontend services`, return nil } - NewHaProxy(s.TemplatesPath, s.ConfigsPath, map[string]bool{}).CreateConfigFromTemplates() + NewHaProxy(s.TemplatesPath, s.ConfigsPath).CreateConfigFromTemplates() s.Equal(expectedData, actualData) } diff --git a/server.go b/server.go index 9c906b02..b4edf96c 100644 --- a/server.go +++ b/server.go @@ -41,6 +41,8 @@ type Serve struct { var serverImpl = Serve{} var cert server.Certer = server.NewCert("/certs") var reload actions.Reloader = actions.NewReload() +//exposed as global so can be changed in tests +var usersBasePath string = "/run/secrets/dfp_users_%s" func (m *Serve) Execute(args []string) error { if proxy.Instance == nil { @@ -267,15 +269,16 @@ func (m *Serve) getService(sd []proxy.ServiceDest, req *http.Request) proxy.Serv if len(req.URL.Query().Get("users")) > 0 { appendUsersFromString(&sr, req.URL.Query().Get("users")) - } else if len(req.URL.Query().Get("usersPath")) > 0 { - usersPath := req.URL.Query().Get("usersPath"); - if content, err := ioutil.ReadFile(usersPath); err == nil { + } else if len(req.URL.Query().Get("usersSecret")) > 0 { + usersSecret := req.URL.Query().Get("usersSecret") + usersFile := fmt.Sprintf(usersBasePath, usersSecret) + if content, err := ioutil.ReadFile(usersFile); err == nil { userContents := strings.TrimRight(string(content[:]), "\n") appendUsersFromString(&sr, userContents) }else { logPrintf("For service %s it was impossible to load userFile %s due to error %s", - sr.ServiceName, usersPath, err.Error()) - //shouldn't we add some rondom user? if Users is empty the service will be unprotected, + sr.ServiceName, usersFile, err.Error()) + //shouldn't we add some random user? if Users is empty the service will be unprotected, // but obviously someone wanted it to be secured sr.Users = append(sr.Users, proxy.User{ Username: "dummyUser", diff --git a/server_test.go b/server_test.go index 6d284385..48a1b9d3 100644 --- a/server_test.go +++ b/server_test.go @@ -71,6 +71,7 @@ func (s *ServerTestSuite) SetupTest() { s.ResponseWriter = getResponseWriterMock() s.RequestReconfigure, _ = http.NewRequest("GET", s.ReconfigureUrl, nil) s.RequestRemove, _ = http.NewRequest("GET", s.RemoveUrl, nil) + usersBasePath = "./test_configs/%s.txt" httpListenAndServe = func(addr string, handler http.Handler) error { return nil } @@ -718,7 +719,7 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersFromUsersFile_WhenP {Username: "user1", Password: "pass1"}, {Username: "user2", Password: "pass2"}, } - req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersPath=./test_configs/users.txt", nil) + req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersSecret=users", nil) expected, _ := json.Marshal(server.Response{ Status: "OK", ServiceName: s.ServiceName, @@ -894,12 +895,12 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithServiceDomainMatchAll_Wh Status: "OK", ServiceName: s.ServiceName, Service: proxy.Service{ - ServiceName: s.ServiceName, - ReqMode: "http", - ServiceColor: s.ServiceColor, - ServiceDomain: s.ServiceDomain, - OutboundHostname: s.OutboundHostname, - ServiceDest: []proxy.ServiceDest{s.sd}, + ServiceName: s.ServiceName, + ReqMode: "http", + ServiceColor: s.ServiceColor, + ServiceDomain: s.ServiceDomain, + OutboundHostname: s.OutboundHostname, + ServiceDest: []proxy.ServiceDest{s.sd}, ServiceDomainMatchAll: true, }, }) From 0a0ca8faef4044148d46ea62358ef48c1ade3a16 Mon Sep 17 00:00:00 2001 From: SaSol Date: Wed, 1 Mar 2017 15:20:10 +0100 Subject: [PATCH 03/10] users param can contain users without password --- actions/reconfigure.go | 2 +- actions/reconfigure_test.go | 7 +- proxy/ha_proxy.go | 2 +- proxy/types.go | 12 ++- proxy/util.go | 2 + server.go | 138 ++++++++++++++++++++++++----- server_test.go | 170 ++++++++++++++++++++++++++++++++---- 7 files changed, 284 insertions(+), 49 deletions(-) diff --git a/actions/reconfigure.go b/actions/reconfigure.go index a0fc6958..782fae16 100644 --- a/actions/reconfigure.go +++ b/actions/reconfigure.go @@ -407,7 +407,7 @@ backend %s{{$.ServiceName}}-be{{.Port}} func (m *Reconfigure) getUsersList(sr *proxy.Service) string { if len(sr.Users) > 0 { return `userlist {{.ServiceName}}Users{{range .Users}} - user {{.Username}} {{if $.UsersPassEncrypted}}password{{end}}{{if not $.UsersPassEncrypted}}insecure-password{{end}} {{.Password}}{{end}} + user {{.Username}} {{if .PassEncrypted}}password{{end}}{{if not .PassEncrypted}}insecure-password{{end}} {{.Password}}{{end}} ` } diff --git a/actions/reconfigure_test.go b/actions/reconfigure_test.go index d3eb89e9..a2fc5f72 100644 --- a/actions/reconfigure_test.go +++ b/actions/reconfigure_test.go @@ -199,13 +199,12 @@ backend myService-be func (s ReconfigureTestSuite) Test_GetTemplates_AddsHttpAuth_WhenUsersIsPresentAndPasswordsEncrypted() { s.reconfigure.Users = []proxy.User{ - {Username: "user-1", Password: "pass-1"}, - {Username: "user-2", Password: "pass-2"}, + {Username: "user-1", Password: "pass-1", PassEncrypted:true}, + {Username: "user-2", Password: "pass-2", PassEncrypted:false}, } - s.reconfigure.UsersPassEncrypted = true expected := `userlist myServiceUsers user user-1 password pass-1 - user user-2 password pass-2 + user user-2 insecure-password pass-2 backend myService-be diff --git a/proxy/ha_proxy.go b/proxy/ha_proxy.go index 0ab3c645..b60619d1 100644 --- a/proxy/ha_proxy.go +++ b/proxy/ha_proxy.go @@ -195,7 +195,7 @@ func (m HaProxy) getConfigData() ConfigData { if len(usersString) > 0 { d.UserList = "\nuserlist defaultUsers\n" passwordType := "insecure-password"; - if strings.EqualFold(encryptedString ,"true") || strings.EqualFold(encryptedString , "1") { + if strings.EqualFold(encryptedString ,"true") { passwordType = "password"; } users := strings.Split(usersString, ",") diff --git a/proxy/types.go b/proxy/types.go index c9f5008d..4a9dd564 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -1,5 +1,7 @@ package proxy +import "strings" + type ServiceDest struct { // The internal port of a service that should be reconfigured. // The port is used only in the *swarm* mode. @@ -84,7 +86,6 @@ type Service struct { TimeoutTunnel string // A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured. Users []User - UsersPassEncrypted bool ServiceColor string ServicePort string AclCondition string @@ -110,6 +111,11 @@ func (slice Services) Swap(i, j int) { } type User struct { - Username string - Password string + Username string + Password string + PassEncrypted bool +} + +func (user *User) HasPassword() (bool) { + return !strings.EqualFold(user.Password, "") } diff --git a/proxy/util.go b/proxy/util.go index a84c9dec..d7777aee 100644 --- a/proxy/util.go +++ b/proxy/util.go @@ -30,3 +30,5 @@ var GetSecretOrEnvVar = func(key, defaultValue string) string { } return defaultValue } + + diff --git a/server.go b/server.go index b4edf96c..476a500c 100644 --- a/server.go +++ b/server.go @@ -141,7 +141,7 @@ func (m *Serve) reload(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) httpWriterSetContentType(w, "application/json") response := server.Response{ - Status: "OK", + Status: "OK", } js, _ := json.Marshal(response) w.Write(js) @@ -220,12 +220,115 @@ func (m *Serve) reconfigure(w http.ResponseWriter, req *http.Request) { w.Write(js) } -func appendUsersFromString(sr *proxy.Service, commaSeparatedUsers string) { +func getUsersFromString(serviceName, commaSeparatedUsers string, passEncrypted bool) ([]*proxy.User) { + collectedUsers := []*proxy.User{} + if len(commaSeparatedUsers) == 0 { + return collectedUsers + } users := strings.Split(commaSeparatedUsers, ",") for _, user := range users { - userPass := strings.Split(strings.Trim(user, "\n\t "), ":") - sr.Users = append(sr.Users, proxy.User{Username: userPass[0], Password: userPass[1]}) + user = strings.Trim(user, "\n\t ") + if strings.Contains(user, ":") { + userDetails := strings.Split(user, ":") + if len(userDetails) != 2 || len(userDetails[0]) == 0 { + logPrintf("For service %s there is an invalid user with no name or invalid format", + serviceName) + } else { + collectedUsers = append(collectedUsers, &proxy.User{Username: userDetails[0], Password: userDetails[1], PassEncrypted: passEncrypted}) + } + } else { + if len(user) == 0 { + logPrintf("For service %s there is an invalid user with no name or invalid format", + serviceName) + } else { + collectedUsers = append(collectedUsers, &proxy.User{Username: user}) + } + } + } + return collectedUsers +} + +func getUsersFromFile(serviceName, fileName string, passEncrypted bool) ([]*proxy.User, error) { + if len(fileName) > 0 { + usersFile := fmt.Sprintf(usersBasePath, fileName) + + if content, err := ioutil.ReadFile(usersFile); err == nil { + userContents := strings.TrimRight(string(content[:]), "\n") + return getUsersFromString(serviceName,userContents, passEncrypted), nil + } else { + logPrintf("For service %s it was impossible to load userFile %s due to error %s", + serviceName, usersFile, err.Error()) + return []*proxy.User{}, err + } + } else { + return []*proxy.User{}, nil + } +} + +func allUsersHavePasswords(users []*proxy.User) bool { + for _, u := range users { + if !u.HasPassword() { + return false + } + } + return true +} + +func findUserByName(users []*proxy.User, name string) *proxy.User { + for _, u := range users { + if strings.EqualFold(name, u.Username) { + return u + } + } + return nil +} + +func mergeUsers(serviceName, usersParam, usersSecret string, usersPassEncrypted bool, + globalUsersString string, globalUsersEncrypted bool) ([]proxy.User) { + var collectedUsers []*proxy.User + paramUsers := getUsersFromString(serviceName,usersParam, usersPassEncrypted) + fileUsers, _ := getUsersFromFile(serviceName, usersSecret, usersPassEncrypted) + fmt.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!PARAM USERS:%d\n", len(paramUsers)) + if len(paramUsers) > 0 { + if !allUsersHavePasswords(paramUsers) { + if len(usersSecret) == 0 { + fileUsers = getUsersFromString(serviceName, globalUsersString, globalUsersEncrypted) + } + for _, u := range paramUsers { + if !u.HasPassword() { + if userByName := findUserByName(fileUsers, u.Username); userByName != nil { + u.Password = userByName.Password + u.PassEncrypted = userByName.PassEncrypted + } else { + logPrintf("For service %s it was impossible to find password for user %s.", + serviceName, u.Username) + } + } + } + } + collectedUsers = paramUsers + } else { + collectedUsers = fileUsers + } + ret := []proxy.User{} + for _, u := range collectedUsers { + if u.HasPassword() { + ret = append(ret, *u) + } + } + if len(ret) == 0 && (len(usersParam) != 0 || len(usersSecret) != 0) { + //we haven't found any users but they were requested so generating dummy one + ret = append(ret, proxy.User{ + Username: "dummyUser", + Password: strconv.FormatInt(rand.Int63(), 3)}, + ) + } + if len(ret) == 0 { + return nil + } + return ret + } func (m *Serve) getService(sd []proxy.ServiceDest, req *http.Request) proxy.Service { @@ -265,27 +368,14 @@ func (m *Serve) getService(sd []proxy.ServiceDest, req *http.Request) proxy.Serv sr.Distribute = m.getBoolParam(req, "distribute") sr.SslVerifyNone = m.getBoolParam(req, "sslVerifyNone") sr.ServiceDomainMatchAll = m.getBoolParam(req, "serviceDomainMatchAll") - sr.UsersPassEncrypted = m.getBoolParam(req, "usersPassEncrypted") - if len(req.URL.Query().Get("users")) > 0 { - appendUsersFromString(&sr, req.URL.Query().Get("users")) - } else if len(req.URL.Query().Get("usersSecret")) > 0 { - usersSecret := req.URL.Query().Get("usersSecret") - usersFile := fmt.Sprintf(usersBasePath, usersSecret) - if content, err := ioutil.ReadFile(usersFile); err == nil { - userContents := strings.TrimRight(string(content[:]), "\n") - appendUsersFromString(&sr, userContents) - }else { - logPrintf("For service %s it was impossible to load userFile %s due to error %s", - sr.ServiceName, usersFile, err.Error()) - //shouldn't we add some random user? if Users is empty the service will be unprotected, - // but obviously someone wanted it to be secured - sr.Users = append(sr.Users, proxy.User{ - Username: "dummyUser", - Password: strconv.FormatInt(rand.Int63(), 3)}, - ) - } - } + globalUsersString := proxy.GetSecretOrEnvVar("USERS", "") + globalUsersEncrypted := strings.EqualFold(proxy.GetSecretOrEnvVar("USERS_PASS_ENCRYPTED", ""), "true") + sr.Users = mergeUsers(sr.ServiceName, + req.URL.Query().Get("users"), + req.URL.Query().Get("usersSecret"), + m.getBoolParam(req, "usersPassEncrypted"), + globalUsersString, globalUsersEncrypted) return sr } diff --git a/server_test.go b/server_test.go index 48a1b9d3..8cd1be9c 100644 --- a/server_test.go +++ b/server_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "time" + "github.com/docker/docker/pkg/testutil/assert" ) type ServerTestSuite struct { @@ -497,17 +498,17 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJSON_WhenUrlIsReconfigure() { func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJSONWithAllPortsAndPaths() { sd := []proxy.ServiceDest{ - proxy.ServiceDest{ + { ServicePath: []string{"/path/to/my-service"}, Port: "1111", SrcPort: 2222, }, - proxy.ServiceDest{ + { ServicePath: []string{"/path/to/my-service-1"}, Port: "3333", SrcPort: 4444, }, - proxy.ServiceDest{ + { ServicePath: []string{"/path/to/my-service-2"}, Port: "4444", }, @@ -642,7 +643,7 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithMode_WhenPresent() { ReqPathSearch: search, ReqPathReplace: replace, ServiceDest: []proxy.ServiceDest{ - proxy.ServiceDest{ + { ServicePath: []string{"/path/to/my/service/api", "/path/to/my/other/service/api"}, SrcPort: 1234, Port: "4321", @@ -690,8 +691,8 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithTemplatePaths_WhenPresen func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsers_WhenPresent() { users := []proxy.User{ - {Username: "user1", Password: "pass1"}, - {Username: "user2", Password: "pass2"}, + {Username: "user1", Password: "pass1", PassEncrypted:false}, + {Username: "user2", Password: "pass2", PassEncrypted:false}, } req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&users=user1:pass1,user2:pass2", nil) expected, _ := json.Marshal(server.Response{ @@ -714,10 +715,36 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsers_WhenPresent() { s.ResponseWriter.AssertCalled(s.T(), "Write", []byte(expected)) } +func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersAndPassEncrypted_WhenPresent() { + users := []proxy.User{ + {Username: "user1", Password: "pass1", PassEncrypted:true}, + {Username: "user2", Password: "pass2", PassEncrypted:true}, + } + req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&users=user1:pass1,user2:pass2&usersPassEncrypted=true", nil) + expected, _ := json.Marshal(server.Response{ + Status: "OK", + ServiceName: s.ServiceName, + Service: proxy.Service{ + ReqMode: "http", + ServiceName: s.ServiceName, + ServiceColor: s.ServiceColor, + ServiceDomain: s.ServiceDomain, + OutboundHostname: s.OutboundHostname, + Users: users, + ServiceDest: []proxy.ServiceDest{s.sd}, + }, + }) + + srv := Serve{} + srv.ServeHTTP(s.ResponseWriter, req) + + s.ResponseWriter.AssertCalled(s.T(), "Write", []byte(expected)) +} + func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersFromUsersFile_WhenPresent() { users := []proxy.User{ - {Username: "user1", Password: "pass1"}, - {Username: "user2", Password: "pass2"}, + {Username: "user1", Password: "pass1", PassEncrypted:false}, + {Username: "user2", Password: "pass2", PassEncrypted:false}, } req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersSecret=users", nil) expected, _ := json.Marshal(server.Response{ @@ -741,18 +768,22 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersFromUsersFile_WhenP } func (s *ServerTestSuite) Test_ServeHTTP_ReturnsJsonWithUsersPassEncrypted_WhenPresent() { - req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersPassEncrypted=true", nil) + users := []proxy.User{ + {Username: "user1", Password: "pass1", PassEncrypted:true}, + {Username: "user2", Password: "pass2", PassEncrypted:true}, + } + req, _ := http.NewRequest("GET", s.ReconfigureUrl+"&usersSecret=users&usersPassEncrypted=true", nil) expected, _ := json.Marshal(server.Response{ Status: "OK", ServiceName: s.ServiceName, Service: proxy.Service{ - ReqMode: "http", - ServiceName: s.ServiceName, - ServiceColor: s.ServiceColor, - ServiceDomain: s.ServiceDomain, - OutboundHostname: s.OutboundHostname, - UsersPassEncrypted: true, - ServiceDest: []proxy.ServiceDest{s.sd}, + ReqMode: "http", + ServiceName: s.ServiceName, + ServiceColor: s.ServiceColor, + ServiceDomain: s.ServiceDomain, + OutboundHostname: s.OutboundHostname, + Users: users, + ServiceDest: []proxy.ServiceDest{s.sd}, }, }) @@ -1279,6 +1310,113 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsStatus500_WhenReadFileFails() { s.ResponseWriter.AssertCalled(s.T(), "WriteHeader", 500) } +func (s *ServerTestSuite) Test_getUsersFromString_AllCases(){ + users := getUsersFromString("sn","u:p", false) + assert.DeepEqual(s.T(),users, []*proxy.User{ + {PassEncrypted: false, Password: "p", Username: "u"}, + }) + + users = getUsersFromString("sn","u:p", true) + assert.DeepEqual(s.T(),users, []*proxy.User{ + {PassEncrypted: true, Password: "p", Username: "u"}, + }) + + users = getUsersFromString("sn","u", false) + assert.DeepEqual(s.T(),users, []*proxy.User{ + {PassEncrypted: false, Password: "", Username: "u"}, + }) + + users = getUsersFromString("sn","u , uu ", false) + assert.DeepEqual(s.T(),users, []*proxy.User{ + {PassEncrypted: false, Password: "", Username: "u"}, + {PassEncrypted: false, Password: "", Username: "uu"}, + }) + + users = getUsersFromString("sn","", false) + assert.DeepEqual(s.T(),users, []*proxy.User{ + }) +} + + +func (s *ServerTestSuite) Test_UsersMerge_AllCases(){ + users := mergeUsers("someService", "user1:pass1,user2:pass2", "", false, "", false) + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "pass1", Username: "user1"}, + {PassEncrypted: false, Password: "pass2", Username: "user2"}, + }) + users = mergeUsers("someService", "user1:pass1,user2", "", false, "", false) + //user without password will not be included + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "pass1", Username: "user1"}, + }) + users = mergeUsers("someService", "user1:passWoRd,user2", "users", false, "", false) + //user2 password will come from users file + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: false, Password: "pass2", Username: "user2"}, + }) + + users = mergeUsers("someService", "user1:passWoRd,user2", "users", true, "", false) + //user2 password will come from users file, all encrypted + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: true, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: true, Password: "pass2", Username: "user2"}, + }) + + users = mergeUsers("someService", "user1:passWoRd,user2", "users", false, "user1:pass1,user2:pass2", false) + //user2 password will come from users file, but not from global one + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: false, Password: "pass2", Username: "user2"}, + }) + + + users = mergeUsers("someService", "user1:passWoRd,user2", "", false, "user1:pass1,user2:pass2", false) + //user2 password will come from global file + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: false, Password: "pass2", Username: "user2"}, + }) + + users = mergeUsers("someService", "user1:passWoRd,user2", "", false, "user1:pass1,user2:pass2", true) + //user2 password will come from global file, globals encrypted only + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: true, Password: "pass2", Username: "user2"}, + }) + + + users = mergeUsers("someService", "user1:passWoRd,user2", "", true, "user1:pass1,user2:pass2", true) + //user2 password will come from global file, all encrypted + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: true, Password: "passWoRd", Username: "user1"}, + {PassEncrypted: true, Password: "pass2", Username: "user2"}, + }) + + + users = mergeUsers("someService", "user1,user2", "", false, "", false) + //no users found dummy one generated + assert.Equal(s.T(), len(users), 1) + assert.Equal(s.T(), users[0].Username, "dummyUser") + + + users = mergeUsers("someService", "", "users", false, "", false) + //Users from file only + assert.DeepEqual(s.T(),users, []proxy.User{ + {PassEncrypted: false, Password: "pass1", Username: "user1"}, + {PassEncrypted: false, Password: "pass2", Username: "user2"}, + }) + + + users = mergeUsers("someService", "", "", false, "user1:pass1,user2:pass2", false) + //No users when only globals present + assert.Equal(s.T(), len(users), 0) + + + +} + + // Suite func TestServerUnitTestSuite(t *testing.T) { From bce61a4172043f4eb6af7325554183783082c215 Mon Sep 17 00:00:00 2001 From: SaSol Date: Wed, 1 Mar 2017 15:55:39 +0100 Subject: [PATCH 04/10] documentation for encrypting passwords --- docs/config.md | 3 ++- docs/feedback-and-contribution.md | 11 ++++++++++ docs/swarm-mode-auto.md | 34 +++++++++++++++++++++++++++++++ docs/usage.md | 6 +++++- server.go | 1 - 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index b9d7cbc5..b76261e7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -32,7 +32,8 @@ The following environment variables can be used to configure the *Docker Flow Pr |TIMEOUT_TUNNEL |The tunnel timeout in seconds |No |3600 |1800 | |TIMEOUT_HTTP_REQUEST|The HTTP request timeout in seconds |No |5 |3 | |TIMEOUT_HTTP_KEEP_ALIVE|The HTTP keep alive timeout in seconds |No |15 |10 | -|USERS |A comma-separated list of credentials(:) for HTTP basic auth, which applies to all the backend routes.|No| |user1:pass1, user2:pass2| +|USERS |A comma-separated list of credentials(:) for HTTP basic auth, which applies to all the backend routes. Presence of /run/secrets/dfp_users file overrides this setting, when is present credentials are read from it. |No| |user1:pass1, user2:pass2| +|USERS_PASS_ENCRYPTED| Indicates if passwords provided through USERS or /run/secrets/dfp_users file are encrypted. Passwords can be encrypted like this: `mkpasswd -m sha-512 password1` |No| false |true| ## Secrets diff --git a/docs/feedback-and-contribution.md b/docs/feedback-and-contribution.md index 8296d3f5..192168bd 100644 --- a/docs/feedback-and-contribution.md +++ b/docs/feedback-and-contribution.md @@ -82,6 +82,17 @@ docker push $DOCKER_HUB_USER/docker-flow-proxy:beta docker-compose -f docker-compose-test.yml run --rm staging-swarm ``` +##### Locally simulating CI + +All above can be run in same manner as CI is running them before build using: +```bash +./scripts/local-ci.sh +``` +script requires: +* DOCKER_HUB_USER environment variable to be set +* HOST_IP to be set +* docker logged in to docker hub with $DOCKER_HUB_USER user + ### Pull Request Once the feature is done, create a pull request. \ No newline at end of file diff --git a/docs/swarm-mode-auto.md b/docs/swarm-mode-auto.md index aa6a78cb..a57d55e7 100644 --- a/docs/swarm-mode-auto.md +++ b/docs/swarm-mode-auto.md @@ -533,6 +533,40 @@ The first request should return the status code `401 Unauthorized` while the sec Please note that both *global* and *service* authentication can be combined. In that case, all services would be protected with the users specified through the `proxy` environment variable `USERS` and individual services could overwrite that through the `reconfigure` parameter `users`. +Please note that user password should not be provided in clear text. Above case is just an example but you should consider encrypting them. Passwords will be persisted in HAProxy configuration and they will be visible while inspecting service details in docker. To encrypt them you should use `mkpasswd` utility and set parameter 'com.df.usersPassEncrypted=true' for passwords provided in `com.df.users` label or environment variable `USERS_PASS_ENCRYPTED` when using `USERS` variable. To show that let's restart the `go-demo`: + +```bash +docker service rm go-demo +docker service create --name go-demo \ + -e DB=go-demo-db \ + --network go-demo \ + --network proxy \ + --label com.df.notify=true \ + --label com.df.distribute=true \ + --label com.df.servicePath=/demo \ + --label com.df.port=8080 \ + --label com.df.usersPassEncrypted=true \ + --label com.df.users=admin:$6$F2eJJA.G$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0 \ + vfarcic/go-demo +``` + +In above example password was encrypted with command: + +```bash +mkpasswd -m sha-512 password +``` + +You can verify it by running again: + +```bash +curl -i $(docker-machine ip node-1)/demo/hello + +curl -i -u admin:password \ + $(docker-machine ip node-1)/demo/hello +``` + +In case we want to use the same set of users to protect a group of services you can also utilize `com.df.usersSecret` label which should contain a name of a secret mounted in Docker Flow Proxy. Secret should be mounted in /run/secrets/dfp_users_* file. For example if `com.df.usersSecret` is set to `monitoring`, proxy expects file /run/secrets/dfp_users_monitoring to be present and to contain user credentials definition. This way multiple services can share same user set. + Before we move into the next subject, please remove the service and create it again without authentication. ```bash diff --git a/docs/usage.md b/docs/usage.md index e803bf01..38240bd8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -42,7 +42,11 @@ The following query parameters can be used when `reqMode` is set to `http` or is |srcPort |The source (entry) port of a service. Useful only when specifying multiple destinations of a single service. The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `srcPort.1`, `srcPort.2`, and so on).|No| |80| |templateBePath|The path to the template representing a snippet of the backend configuration. If specified, the backend template will be loaded from the specified file. If specified, `templateFePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/be.tmpl| |templateFePath|The path to the template representing a snippet of the frontend configuration. If specified, the frontend template will be loaded from the specified file. If specified, `templateBePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/fe.tmpl| -|users |A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured.|No| |usr1:pwd1,usr2:pwd2| +|users |A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured. If used with usersSecret or when USERS environment variable is set password may be omitted - it will be taken from usersSecret file or global configuration if usersSecret is not present. |No| |usr1:pwd1,usr2:pwd2| +|usersSecret |Name of file prefixed /run/secrets/dfp_users_%s from which credentials will be taken for this service. /run/secrets/dfp_users_* files must be a comma-separated list of credentials(:) |No| |monitoring| +|usersPassEncrypted| Indicates that passwords provided by users or usersSecret contain encrypted passwords. Passwords can be encrypted like this: `mkpasswd -m sha-512 password1` |No| false |true| + + The following query parameters can be used when `reqMode` is set to `tcp`. diff --git a/server.go b/server.go index 476a500c..67b2cd26 100644 --- a/server.go +++ b/server.go @@ -288,7 +288,6 @@ func mergeUsers(serviceName, usersParam, usersSecret string, usersPassEncrypted var collectedUsers []*proxy.User paramUsers := getUsersFromString(serviceName,usersParam, usersPassEncrypted) fileUsers, _ := getUsersFromFile(serviceName, usersSecret, usersPassEncrypted) - fmt.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!PARAM USERS:%d\n", len(paramUsers)) if len(paramUsers) > 0 { if !allUsersHavePasswords(paramUsers) { if len(usersSecret) == 0 { From 839a314197349c8783145dc36f301acb311a1cd2 Mon Sep 17 00:00:00 2001 From: SaSol Date: Thu, 2 Mar 2017 09:36:49 +0100 Subject: [PATCH 05/10] documentation update regarding usage of usersSecret param --- docs/swarm-mode-auto.md | 49 ++++++++++++++++++++++++++++------------- docs/usage.md | 2 +- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/swarm-mode-auto.md b/docs/swarm-mode-auto.md index a57d55e7..9a675d68 100644 --- a/docs/swarm-mode-auto.md +++ b/docs/swarm-mode-auto.md @@ -533,40 +533,59 @@ The first request should return the status code `401 Unauthorized` while the sec Please note that both *global* and *service* authentication can be combined. In that case, all services would be protected with the users specified through the `proxy` environment variable `USERS` and individual services could overwrite that through the `reconfigure` parameter `users`. -Please note that user password should not be provided in clear text. Above case is just an example but you should consider encrypting them. Passwords will be persisted in HAProxy configuration and they will be visible while inspecting service details in docker. To encrypt them you should use `mkpasswd` utility and set parameter 'com.df.usersPassEncrypted=true' for passwords provided in `com.df.users` label or environment variable `USERS_PASS_ENCRYPTED` when using `USERS` variable. To show that let's restart the `go-demo`: +Above all note that user password should not be provided in clear text. Above case is just an example but you should consider encrypting them. Passwords will be persisted in HAProxy configuration and they will be visible while inspecting service details in docker. To encrypt them you should use `mkpasswd` utility and set parameter 'com.df.usersPassEncrypted=true' for passwords provided in `com.df.users` label or environment variable `USERS_PASS_ENCRYPTED` when using `USERS` variable. +To show that let's update the `go-demo`. First lets hash password: ```bash -docker service rm go-demo -docker service create --name go-demo \ - -e DB=go-demo-db \ - --network go-demo \ - --network proxy \ - --label com.df.notify=true \ - --label com.df.distribute=true \ - --label com.df.servicePath=/demo \ - --label com.df.port=8080 \ - --label com.df.usersPassEncrypted=true \ - --label com.df.users=admin:$6$F2eJJA.G$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0 \ - vfarcic/go-demo +mkpasswd -m sha-512 password ``` -In above example password was encrypted with command: +This should output something like `$6$F2eJJA.G$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0`. Be aware that `$` signs needs to be escaped. In `mkpasswd` output there will be always 3 `$` characters. Let's update out go demo service: ```bash -mkpasswd -m sha-512 password +docker service update \ + --label-add com.df.usersPassEncrypted=true \ + --label-add com.df.users=admin:\$6\$F2eJJA.G\$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0 \ + go-demo ``` You can verify it by running again: ```bash curl -i $(docker-machine ip node-1)/demo/hello +``` +Which will fail with `HTTP/1.0 401 Unauthorized` as expected, but with proper password it will work: +```bash curl -i -u admin:password \ $(docker-machine ip node-1)/demo/hello ``` In case we want to use the same set of users to protect a group of services you can also utilize `com.df.usersSecret` label which should contain a name of a secret mounted in Docker Flow Proxy. Secret should be mounted in /run/secrets/dfp_users_* file. For example if `com.df.usersSecret` is set to `monitoring`, proxy expects file /run/secrets/dfp_users_monitoring to be present and to contain user credentials definition. This way multiple services can share same user set. +To show how it works lets create passwords secret for our proxy: +```bash +echo "observer:\$6\$F2eJJA.G\$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0" \ + | docker secret create dfp_users_monitoring - +docker service update \ + --secret-add dfp_users_monitoring \ + proxy +``` +Now we need to change configuration of our test service so that proxy will fetch users for it from this secret: + +```bash +docker service update \ + --label-rm com.df.users \ + --label-add com.df.usersSecret=monitoring \ + go-demo +``` +Now we can verify that aour service is reachable with `observer` user: +```bash +curl -i -u observer:password \ + $(docker-machine ip node-1)/demo/hello +``` + + Before we move into the next subject, please remove the service and create it again without authentication. ```bash diff --git a/docs/usage.md b/docs/usage.md index 38240bd8..aeec0c04 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -43,7 +43,7 @@ The following query parameters can be used when `reqMode` is set to `http` or is |templateBePath|The path to the template representing a snippet of the backend configuration. If specified, the backend template will be loaded from the specified file. If specified, `templateFePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/be.tmpl| |templateFePath|The path to the template representing a snippet of the frontend configuration. If specified, the frontend template will be loaded from the specified file. If specified, `templateBePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/fe.tmpl| |users |A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured. If used with usersSecret or when USERS environment variable is set password may be omitted - it will be taken from usersSecret file or global configuration if usersSecret is not present. |No| |usr1:pwd1,usr2:pwd2| -|usersSecret |Name of file prefixed /run/secrets/dfp_users_%s from which credentials will be taken for this service. /run/secrets/dfp_users_* files must be a comma-separated list of credentials(:) |No| |monitoring| +|usersSecret | Suffix of file name from which credentials will be taken for this service. Files must be a comma-separated list of credentials(:). This suffix will be appended to '/run/secrets/dfp_users_' path, so when its value is `mysecrets` expected file to be found will be `/run/secrets/dfp_users_mysecrets`. |No| |monitoring| |usersPassEncrypted| Indicates that passwords provided by users or usersSecret contain encrypted passwords. Passwords can be encrypted like this: `mkpasswd -m sha-512 password1` |No| false |true| From 813942c373121ece959bb994064bbda22a0dc28c Mon Sep 17 00:00:00 2001 From: SaSol Date: Thu, 2 Mar 2017 11:08:07 +0100 Subject: [PATCH 06/10] single point of parsing users files, allowing new line to be separator --- proxy/ha_proxy.go | 16 ++++---- proxy/types.go | 53 ++++++++++++++++++++++++- proxy/types_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++ proxy/util.go | 2 + server.go | 40 +++---------------- server_test.go | 25 ------------ 6 files changed, 163 insertions(+), 68 deletions(-) create mode 100644 proxy/types_test.go diff --git a/proxy/ha_proxy.go b/proxy/ha_proxy.go index b60619d1..eaf2772d 100644 --- a/proxy/ha_proxy.go +++ b/proxy/ha_proxy.go @@ -194,15 +194,17 @@ func (m HaProxy) getConfigData() ConfigData { encryptedString := GetSecretOrEnvVar("USERS_PASS_ENCRYPTED", "") if len(usersString) > 0 { d.UserList = "\nuserlist defaultUsers\n" - passwordType := "insecure-password"; - if strings.EqualFold(encryptedString ,"true") { - passwordType = "password"; + encrypted :=strings.EqualFold(encryptedString ,"true") + users := ExtractUsersFromString("globalUsers", usersString, encrypted, true) + if len(users) == 0 { + users = append(users, RandomUser()) } - users := strings.Split(usersString, ",") for _, user := range users { - //trimming to allow new lines in file - userPass := strings.Split(strings.Trim(user, "\n\t "), ":") - d.UserList = fmt.Sprintf("%s user %s %s %s\n", d.UserList, userPass[0], passwordType, userPass[1]) + passwordType := "insecure-password" + if user.PassEncrypted { + passwordType = "password" + } + d.UserList = fmt.Sprintf("%s user %s %s %s\n", d.UserList, user.Username, passwordType, user.Password) } } if strings.EqualFold(GetSecretOrEnvVar("DEBUG", ""), "true") { diff --git a/proxy/types.go b/proxy/types.go index 4a9dd564..d7e0bf90 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -1,6 +1,10 @@ package proxy -import "strings" +import ( + "strings" + "strconv" + "math/rand" +) type ServiceDest struct { // The internal port of a service that should be reconfigured. @@ -119,3 +123,50 @@ type User struct { func (user *User) HasPassword() (bool) { return !strings.EqualFold(user.Password, "") } + +func RandomUser() *User { + return &User{ + Username: "dummyUser", + PassEncrypted: true, + Password: strconv.FormatInt(rand.Int63(), 3)} +} + +func ExtractUsersFromString(context, usersString string, encrypted, skipEmptyPassword bool) ([]*User) { + collectedUsers := []*User{} + if len(usersString) == 0 { + return collectedUsers + } + splitter := func(x rune) bool { + return x == '\n' || x == ',' + } + users := strings.FieldsFunc(usersString, splitter) + for _, user := range users { + user = strings.Trim(user, "\n\t ") + if len(user) == 0 { + continue + } + if strings.Contains(user, ":") { + colonIndex := strings.Index(user, ":") + userName := strings.Trim(user[0:colonIndex], "\t ") + userPass := strings.Trim(user[colonIndex+1:], "\t ") + if len(userName) == 0 || len(userPass) == 0 { + logPrintf("For service %s there is an invalid user with no name or invalid format", + context) + } else { + collectedUsers = append(collectedUsers, &User{Username: userName, Password: userPass, PassEncrypted: encrypted}) + } + } else { + if len(user) == 0 { + logPrintf("For service %s there is an invalid user with no name or invalid format", + context) + } else if skipEmptyPassword { + logPrintf("For service %s there is an user %s with no password which is not allowed here", + context, user) + } else if !skipEmptyPassword { + collectedUsers = append(collectedUsers, &User{Username: user}) + } + } + } + return collectedUsers +} + diff --git a/proxy/types_test.go b/proxy/types_test.go new file mode 100644 index 00000000..7f8b3b8e --- /dev/null +++ b/proxy/types_test.go @@ -0,0 +1,95 @@ +package proxy + +import ( + "github.com/stretchr/testify/suite" + "testing" + "github.com/docker/docker/pkg/testutil/assert" +) + +type TypesTestSuite struct { + suite.Suite +} + +func (s *TypesTestSuite) SetupTest() { + logPrintf = func(format string, v ...interface{}) {} +} + +// NewRun + +func (s TypesTestSuite) Test_ExtractUsersFromString() { + + users := ExtractUsersFromString("sn","u:p", false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "p", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u:p", true, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: true, Password: "p", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u:p:2", true, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: true, Password: "p:2", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u", false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u:p,ww", false, true) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "p", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u:p,ww:,:asd", false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "p", Username: "u"}, + }) + + users = ExtractUsersFromString("sn","u , uu ", false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "", Username: "u"}, + {PassEncrypted: false, Password: "", Username: "uu"}, + }) + + users = ExtractUsersFromString("sn","", false, false) + assert.DeepEqual(s.T(),users, []*User{ + }) + + users = ExtractUsersFromString("sn",`u , + uu `, false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "", Username: "u"}, + {PassEncrypted: false, Password: "", Username: "uu"}, + }) + users = ExtractUsersFromString("sn",`u +uu`, false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "", Username: "u"}, + {PassEncrypted: false, Password: "", Username: "uu"}, + }) + + + users = ExtractUsersFromString("sn", +`u:p +uu:pp, +uuu:ppp + +, + +x:X`, false, false) + assert.DeepEqual(s.T(),users, []*User{ + {PassEncrypted: false, Password: "p", Username: "u"}, + {PassEncrypted: false, Password: "pp", Username: "uu"}, + {PassEncrypted: false, Password: "ppp", Username: "uuu"}, + {PassEncrypted: false, Password: "X", Username: "x"}, + }) +} + +// Suite + +func TestRunUnitTestSuite(t *testing.T) { + suite.Run(t, new(TypesTestSuite)) +} diff --git a/proxy/util.go b/proxy/util.go index d7777aee..d9316d5b 100644 --- a/proxy/util.go +++ b/proxy/util.go @@ -32,3 +32,5 @@ var GetSecretOrEnvVar = func(key, defaultValue string) string { } + + diff --git a/server.go b/server.go index 67b2cd26..69162524 100644 --- a/server.go +++ b/server.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" "io/ioutil" - "math/rand" ) // TODO: Move to server package @@ -220,33 +219,7 @@ func (m *Serve) reconfigure(w http.ResponseWriter, req *http.Request) { w.Write(js) } -func getUsersFromString(serviceName, commaSeparatedUsers string, passEncrypted bool) ([]*proxy.User) { - collectedUsers := []*proxy.User{} - if len(commaSeparatedUsers) == 0 { - return collectedUsers - } - users := strings.Split(commaSeparatedUsers, ",") - for _, user := range users { - user = strings.Trim(user, "\n\t ") - if strings.Contains(user, ":") { - userDetails := strings.Split(user, ":") - if len(userDetails) != 2 || len(userDetails[0]) == 0 { - logPrintf("For service %s there is an invalid user with no name or invalid format", - serviceName) - } else { - collectedUsers = append(collectedUsers, &proxy.User{Username: userDetails[0], Password: userDetails[1], PassEncrypted: passEncrypted}) - } - } else { - if len(user) == 0 { - logPrintf("For service %s there is an invalid user with no name or invalid format", - serviceName) - } else { - collectedUsers = append(collectedUsers, &proxy.User{Username: user}) - } - } - } - return collectedUsers -} + func getUsersFromFile(serviceName, fileName string, passEncrypted bool) ([]*proxy.User, error) { if len(fileName) > 0 { @@ -254,7 +227,7 @@ func getUsersFromFile(serviceName, fileName string, passEncrypted bool) ([]*prox if content, err := ioutil.ReadFile(usersFile); err == nil { userContents := strings.TrimRight(string(content[:]), "\n") - return getUsersFromString(serviceName,userContents, passEncrypted), nil + return proxy.ExtractUsersFromString(serviceName,userContents, passEncrypted, true), nil } else { logPrintf("For service %s it was impossible to load userFile %s due to error %s", serviceName, usersFile, err.Error()) @@ -286,12 +259,12 @@ func findUserByName(users []*proxy.User, name string) *proxy.User { func mergeUsers(serviceName, usersParam, usersSecret string, usersPassEncrypted bool, globalUsersString string, globalUsersEncrypted bool) ([]proxy.User) { var collectedUsers []*proxy.User - paramUsers := getUsersFromString(serviceName,usersParam, usersPassEncrypted) + paramUsers := proxy.ExtractUsersFromString(serviceName,usersParam, usersPassEncrypted, false) fileUsers, _ := getUsersFromFile(serviceName, usersSecret, usersPassEncrypted) if len(paramUsers) > 0 { if !allUsersHavePasswords(paramUsers) { if len(usersSecret) == 0 { - fileUsers = getUsersFromString(serviceName, globalUsersString, globalUsersEncrypted) + fileUsers = proxy.ExtractUsersFromString(serviceName, globalUsersString, globalUsersEncrypted, true) } for _, u := range paramUsers { if !u.HasPassword() { @@ -318,10 +291,7 @@ func mergeUsers(serviceName, usersParam, usersSecret string, usersPassEncrypted } if len(ret) == 0 && (len(usersParam) != 0 || len(usersSecret) != 0) { //we haven't found any users but they were requested so generating dummy one - ret = append(ret, proxy.User{ - Username: "dummyUser", - Password: strconv.FormatInt(rand.Int63(), 3)}, - ) + ret = append(ret, *proxy.RandomUser()) } if len(ret) == 0 { return nil diff --git a/server_test.go b/server_test.go index 8cd1be9c..00c7236d 100644 --- a/server_test.go +++ b/server_test.go @@ -1310,32 +1310,7 @@ func (s *ServerTestSuite) Test_ServeHTTP_ReturnsStatus500_WhenReadFileFails() { s.ResponseWriter.AssertCalled(s.T(), "WriteHeader", 500) } -func (s *ServerTestSuite) Test_getUsersFromString_AllCases(){ - users := getUsersFromString("sn","u:p", false) - assert.DeepEqual(s.T(),users, []*proxy.User{ - {PassEncrypted: false, Password: "p", Username: "u"}, - }) - - users = getUsersFromString("sn","u:p", true) - assert.DeepEqual(s.T(),users, []*proxy.User{ - {PassEncrypted: true, Password: "p", Username: "u"}, - }) - - users = getUsersFromString("sn","u", false) - assert.DeepEqual(s.T(),users, []*proxy.User{ - {PassEncrypted: false, Password: "", Username: "u"}, - }) - users = getUsersFromString("sn","u , uu ", false) - assert.DeepEqual(s.T(),users, []*proxy.User{ - {PassEncrypted: false, Password: "", Username: "u"}, - {PassEncrypted: false, Password: "", Username: "uu"}, - }) - - users = getUsersFromString("sn","", false) - assert.DeepEqual(s.T(),users, []*proxy.User{ - }) -} func (s *ServerTestSuite) Test_UsersMerge_AllCases(){ From b1145182004990414863e947e958c650d68a1051 Mon Sep 17 00:00:00 2001 From: Viktor Farcic Date: Thu, 2 Mar 2017 22:32:39 +0100 Subject: [PATCH 07/10] Update config.md --- docs/config.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index b76261e7..0acf254a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -32,8 +32,8 @@ The following environment variables can be used to configure the *Docker Flow Pr |TIMEOUT_TUNNEL |The tunnel timeout in seconds |No |3600 |1800 | |TIMEOUT_HTTP_REQUEST|The HTTP request timeout in seconds |No |5 |3 | |TIMEOUT_HTTP_KEEP_ALIVE|The HTTP keep alive timeout in seconds |No |15 |10 | -|USERS |A comma-separated list of credentials(:) for HTTP basic auth, which applies to all the backend routes. Presence of /run/secrets/dfp_users file overrides this setting, when is present credentials are read from it. |No| |user1:pass1, user2:pass2| -|USERS_PASS_ENCRYPTED| Indicates if passwords provided through USERS or /run/secrets/dfp_users file are encrypted. Passwords can be encrypted like this: `mkpasswd -m sha-512 password1` |No| false |true| +|USERS             |A comma-separated list of credentials(:) for HTTP basic auth, which applies to all the backend routes. Presence of `dfp_users` Docker secret (`/run/secrets/dfp_users file`) overrides this setting. When present, credentials are read from it. |No| |user1:pass1, user2:pass2| +|USERS_PASS_ENCRYPTED| Indicates if passwords provided through USERS or Docker secret `dfp_users` (`/run/secrets/dfp_users` file) are encrypted. Passwords can be encrypted with the `mkpasswd -m sha-512 my-password` command |No| false |true| ## Secrets From 582d3cee77db6baf3fc71ea81c12f3a7c6982730 Mon Sep 17 00:00:00 2001 From: Viktor Farcic Date: Thu, 2 Mar 2017 22:33:58 +0100 Subject: [PATCH 08/10] Update feedback-and-contribution.md --- docs/feedback-and-contribution.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/feedback-and-contribution.md b/docs/feedback-and-contribution.md index 192168bd..b5276c1e 100644 --- a/docs/feedback-and-contribution.md +++ b/docs/feedback-and-contribution.md @@ -84,15 +84,18 @@ docker-compose -f docker-compose-test.yml run --rm staging-swarm ##### Locally simulating CI -All above can be run in same manner as CI is running them before build using: +All above can be executed in same manner as CI is running it before a build using the command that follows. + ```bash ./scripts/local-ci.sh ``` -script requires: + +The script requires: + * DOCKER_HUB_USER environment variable to be set * HOST_IP to be set * docker logged in to docker hub with $DOCKER_HUB_USER user ### Pull Request -Once the feature is done, create a pull request. \ No newline at end of file +Once the feature is done, create a pull request. From e3621e79af7b6dbb72b9870fb193ae0c1d4ea242 Mon Sep 17 00:00:00 2001 From: Viktor Farcic Date: Thu, 2 Mar 2017 22:49:29 +0100 Subject: [PATCH 09/10] Update swarm-mode-auto.md --- docs/swarm-mode-auto.md | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/swarm-mode-auto.md b/docs/swarm-mode-auto.md index 9a675d68..b6e8e1c1 100644 --- a/docs/swarm-mode-auto.md +++ b/docs/swarm-mode-auto.md @@ -533,14 +533,23 @@ The first request should return the status code `401 Unauthorized` while the sec Please note that both *global* and *service* authentication can be combined. In that case, all services would be protected with the users specified through the `proxy` environment variable `USERS` and individual services could overwrite that through the `reconfigure` parameter `users`. -Above all note that user password should not be provided in clear text. Above case is just an example but you should consider encrypting them. Passwords will be persisted in HAProxy configuration and they will be visible while inspecting service details in docker. To encrypt them you should use `mkpasswd` utility and set parameter 'com.df.usersPassEncrypted=true' for passwords provided in `com.df.users` label or environment variable `USERS_PASS_ENCRYPTED` when using `USERS` variable. +Please note that passwords should not be provided in clear text. The above commands were only an example. You should consider encrypting passwords. They will be persisted in HAProxy configuration and they will be visible while inspecting service details in Docker. To encrypt them you should use `mkpasswd` utility and set parameter 'com.df.usersPassEncrypted=true' for passwords provided in `com.df.users` label or environment variable `USERS_PASS_ENCRYPTED` when using `USERS` variable. + +To demonstrated how encrypted passwords work we'll start by hashing a password. -To show that let's update the `go-demo`. First lets hash password: ```bash mkpasswd -m sha-512 password ``` -This should output something like `$6$F2eJJA.G$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0`. Be aware that `$` signs needs to be escaped. In `mkpasswd` output there will be always 3 `$` characters. Let's update out go demo service: +The out should be similar to the one that follows. + +``` +$6$F2eJJA.G$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0 +``` + +Please note that `$` signs needs to be escaped. In `mkpasswd` output there will be always three `$` characters. + +Let's update out go demo service: ```bash docker service update \ @@ -549,29 +558,39 @@ docker service update \ go-demo ``` -You can verify it by running again: +You can verify that the authentication is required by executing the command that follows. ```bash curl -i $(docker-machine ip node-1)/demo/hello ``` -Which will fail with `HTTP/1.0 401 Unauthorized` as expected, but with proper password it will work: +The output should indicate a `HTTP/1.0 401 Unauthorized` failure. + +Let's repeat the request but, this time, with the proper password. + ```bash curl -i -u admin:password \ $(docker-machine ip node-1)/demo/hello ``` -In case we want to use the same set of users to protect a group of services you can also utilize `com.df.usersSecret` label which should contain a name of a secret mounted in Docker Flow Proxy. Secret should be mounted in /run/secrets/dfp_users_* file. For example if `com.df.usersSecret` is set to `monitoring`, proxy expects file /run/secrets/dfp_users_monitoring to be present and to contain user credentials definition. This way multiple services can share same user set. +Since Docker release 1.13, the preferable way to store confidential information is through Docker secrets. *Docker Flow Proxy* supports passwords stored as secrets through the `com.df.usersSecret` label. It should contain a name of a secret mounted in *Docker Flow Proxy*. The name of the secret should be prefixed with `dfp_users_`. For example if `com.df.usersSecret` is set to `monitoring`, proxy expects the secret name to be dfp_users_monitoring. + +To show how it works, lets create a secret with the username `observer` and the hashed password. The commands are as follows. -To show how it works lets create passwords secret for our proxy: ```bash echo "observer:\$6\$F2eJJA.G\$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0" \ | docker secret create dfp_users_monitoring - + docker service update \ --secret-add dfp_users_monitoring \ proxy ``` -Now we need to change configuration of our test service so that proxy will fetch users for it from this secret: + +The first command stored the username and the hashed password as the secret `dfp_users_monitoring`. Username and password were separated with the colon (`:`). + +The second command updated the proxy by adding the secret to it. + +Now we need to change configuration of our test service so that the proxy can get the information about the name of the secret that contains the username and the hashed password. ```bash docker service update \ @@ -579,12 +598,15 @@ docker service update \ --label-add com.df.usersSecret=monitoring \ go-demo ``` -Now we can verify that aour service is reachable with `observer` user: + +We should verify that our service is reachable and protected with the user `observer`. + ```bash curl -i -u observer:password \ $(docker-machine ip node-1)/demo/hello ``` +As expected, the status code of the response is `200`, indicating that the request was successfull. Before we move into the next subject, please remove the service and create it again without authentication. From d4c77ba81f8df3c2a8d4a534e242477d2452b8cd Mon Sep 17 00:00:00 2001 From: Viktor Farcic Date: Thu, 2 Mar 2017 22:55:05 +0100 Subject: [PATCH 10/10] Update usage.md --- docs/usage.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index aeec0c04..ba7b658a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -42,11 +42,9 @@ The following query parameters can be used when `reqMode` is set to `http` or is |srcPort |The source (entry) port of a service. Useful only when specifying multiple destinations of a single service. The parameter can be prefixed with an index thus allowing definition of multiple destinations for a single service (e.g. `srcPort.1`, `srcPort.2`, and so on).|No| |80| |templateBePath|The path to the template representing a snippet of the backend configuration. If specified, the backend template will be loaded from the specified file. If specified, `templateFePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/be.tmpl| |templateFePath|The path to the template representing a snippet of the frontend configuration. If specified, the frontend template will be loaded from the specified file. If specified, `templateBePath` must be set as well. See the [Templates](#templates) section for more info.| | |/tmpl/fe.tmpl| -|users |A comma-separated list of credentials(:) for HTTP basic auth, which applies only to the service that will be reconfigured. If used with usersSecret or when USERS environment variable is set password may be omitted - it will be taken from usersSecret file or global configuration if usersSecret is not present. |No| |usr1:pwd1,usr2:pwd2| -|usersSecret | Suffix of file name from which credentials will be taken for this service. Files must be a comma-separated list of credentials(:). This suffix will be appended to '/run/secrets/dfp_users_' path, so when its value is `mysecrets` expected file to be found will be `/run/secrets/dfp_users_mysecrets`. |No| |monitoring| -|usersPassEncrypted| Indicates that passwords provided by users or usersSecret contain encrypted passwords. Passwords can be encrypted like this: `mkpasswd -m sha-512 password1` |No| false |true| - - +|users       |A comma-separated list of credentials (:) for HTTP basic authentication. It applies only to the service that will be reconfigured. If used with `usersSecret`, or when `USERS` environment variable is set, password may be omitted. In that case, it will be taken from `usersSecret` file or the global configuration if `usersSecret` is not present. |No| |usr1:pwd1, usr2:pwd2| +|usersSecret |Suffix of Docker secret from which credentials will be taken for this service. Files must be a comma-separated list of credentials (:). This suffix will be prepended with `dfp_users_`. For example, if the value is `mysecrets` the expected name of the Docker secret is `dfp_users_mysecrets`.|No| |monitoring| +|usersPassEncrypted|Indicates whether passwords provided by `users` or `usersSecret` contain encrypted data. Passwords can be encrypted with the command `mkpasswd -m sha-512 password1`|No|false|true| The following query parameters can be used when `reqMode` is set to `tcp`.