Skip to content

Commit

Permalink
Merge pull request #157 from s4s0l/issue-146
Browse files Browse the repository at this point in the history
Issue 146 - encrypting passwords
  • Loading branch information
vfarcic authored Mar 2, 2017
2 parents 7b79d2a + d4c77ba commit a8f8d46
Show file tree
Hide file tree
Showing 16 changed files with 665 additions and 57 deletions.
5 changes: 2 additions & 3 deletions actions/reconfigure.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"html/template"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -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
Expand All @@ -408,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}} insecure-password {{.Password}}{{end}}
user {{.Username}} {{if .PassEncrypted}}password{{end}}{{if not .PassEncrypted}}insecure-password{{end}} {{.Password}}{{end}}
`
}
Expand Down
25 changes: 25 additions & 0 deletions actions/reconfigure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,31 @@ 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", PassEncrypted:true},
{Username: "user-2", Password: "pass-2", PassEncrypted:false},
}
expected := `userlist myServiceUsers
user user-1 password pass-1
user user-2 insecure-password pass-2
backend myService-be
mode http
http-request add-header X-Forwarded-Proto https if { ssl_fc }
Expand Down
3 changes: 2 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(<user>:<pass>) for HTTP basic auth, which applies to all the backend routes.|No| |user1:pass1, user2:pass2|
|USERS             |A comma-separated list of credentials(<user>:<pass>) 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

Expand Down
16 changes: 15 additions & 1 deletion docs/feedback-and-contribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ 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 executed in same manner as CI is running it before a build using the command that follows.

```bash
./scripts/local-ci.sh
```

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.
Once the feature is done, create a pull request.
75 changes: 75 additions & 0 deletions docs/swarm-mode-auto.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,81 @@ 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 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.

```bash
mkpasswd -m sha-512 password
```

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 \
--label-add com.df.usersPassEncrypted=true \
--label-add com.df.users=admin:\$6\$F2eJJA.G\$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0 \
go-demo
```

You can verify that the authentication is required by executing the command that follows.

```bash
curl -i $(docker-machine ip node-1)/demo/hello
```

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
```

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.

```bash
echo "observer:\$6\$F2eJJA.G\$BfoxX38MoNS10tywEzQZVDZOAjJn9wyTZJecYg.CymjwE8Rgm7xJn0KG3faT36GZbOtrsu4ba.vhsnHrPCNAa0" \
| docker secret create dfp_users_monitoring -

docker service update \
--secret-add dfp_users_monitoring \
proxy
```

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 \
--label-rm com.df.users \
--label-add com.df.usersSecret=monitoring \
go-demo
```

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.

```bash
Expand Down
4 changes: 3 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +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(<user>:<pass>) 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 (<user>:<pass>) 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 (<user>:<pass>). 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`.

Expand Down
26 changes: 26 additions & 0 deletions integration_tests/integration_swarm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,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("")
Expand Down
57 changes: 28 additions & 29 deletions proxy/ha_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,26 +190,34 @@ func (m HaProxy) getConfigData() ConfigData {
d := ConfigData{
CertsString: strings.Join(certsString, " "),
}
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"
users := strings.Split(usersString, ",")
encrypted :=strings.EqualFold(encryptedString ,"true")
users := ExtractUsersFromString("globalUsers", usersString, encrypted, true)
if len(users) == 0 {
users = append(users, RandomUser())
}
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])
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(m.getSecretOrEnvVar("DEBUG", ""), "true") {
if strings.EqualFold(GetSecretOrEnvVar("DEBUG", ""), "true") {
d.ExtraGlobal += `
debug`
} else {
Expand All @@ -218,18 +226,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 {
Expand Down Expand Up @@ -273,16 +281,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 := ``
Expand Down
32 changes: 32 additions & 0 deletions proxy/ha_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,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).CreateConfigFromTemplates()

s.Equal(expectedData, actualData)
}

func (s HaProxyTestSuite) Test_CreateConfigFromTemplates_ReplacesValuesWithEnvVars() {
tests := []struct {
envKey string
Expand Down
Loading

0 comments on commit a8f8d46

Please sign in to comment.