Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http basic auth #454

Merged
merged 34 commits into from
Jan 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
72f2104
refactor repeated code
Jan 4, 2022
791272e
basicauth validator using userFile
Jan 5, 2022
4847899
format code
Jan 5, 2022
0d4644f
basic auth validator using etcd; cache results to LRU cache
Jan 5, 2022
30944df
fix header parsing
Jan 5, 2022
bc8f83a
reset changes in oauth2
Jan 5, 2022
9316bfa
fix test
Jan 6, 2022
5c81d42
remove unix specific temp file path
Jan 6, 2022
de2250c
sync authorized users
Jan 6, 2022
dd32290
remove syncInterval
Jan 6, 2022
60acff6
add fileWatcher for unvalidating cache
Jan 6, 2022
2b5fddb
remove unused funcs and add documentation
Jan 6, 2022
b188893
fix race condition in unittest
Jan 6, 2022
8bf353e
initialize context to fix test
Jan 6, 2022
5336a8f
simplify test and fix basic auth
Jan 7, 2022
26bd8ef
make etcd yaml password format configurable and use go-htpasswd for b…
Jan 10, 2022
03c435f
better error handling
Jan 10, 2022
0d23ac7
add test case
Jan 10, 2022
a5fd957
make etcd prefix configuratble
Jan 10, 2022
caa1a49
make username yaml entry configurable
Jan 11, 2022
6167196
set x-auth-user header
Jan 11, 2022
eeed6e9
headerlookup filter
Jan 11, 2022
4f2c339
fix typo
Jan 11, 2022
ccf9cca
fix filter
Jan 11, 2022
82796cb
sanitize header keys
Jan 12, 2022
4bbe9ae
force basicAuth and headerLookup to use /custom-data/ etcd prefix
Jan 12, 2022
bc7ff35
simplify configuration by fixing key and password entries; allow skip…
Jan 12, 2022
8cd8421
use fsnotify for basic auth userfile updates
Jan 13, 2022
68d7cf8
go mod tidy
Jan 13, 2022
b4442d6
address review comments
Jan 13, 2022
1565bab
add cache and cache update
Jan 13, 2022
7f5ba26
fix cache update
Jan 13, 2022
7b51d82
fix headerlookup test
Jan 13, 2022
249b3c7
include new cluster test helper to cluster tests
Jan 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions doc/cookbook/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
- [JWT](#jwt)
- [Signature](#signature)
- [OAuth2](#oauth2)
- [Basic Auth](#basic-auth)
- [References](#references)
- [Header](#header-1)
- [JWT](#jwt-1)
- [Signature](#signature-1)
- [OAuth2](#oauth2-1)
- [Basic Auth](#basic-auth-1)
- [Concepts](#concepts)

As a production-ready cloud-native traffic orchestrator, Easegress cares about security and provides several features to ensure that.
Expand Down Expand Up @@ -134,13 +136,36 @@ filters:
insecureTls: false
- name: proxy
kind: Proxy

```

* The example above uses a token introspection server, which is provided by `endpoint` filed for validation. It also supports `Self-Encoded Access Tokens mode` which will require a JWT related configuration included. Check it out in the Easegress filter doc if needed. [5]

* For the full YAML, see [here](#oauth-1)

### Basic Auth

* Using Basic Auth validation in Easegress. Basic access authentication is the simplest technique for enforcing access control to web resources [6]. You can create .htpasswd file using *apache2-util* `htpasswd` [7] for storing encrypted user credentials. Please note that Basic Auth is not the most secure access control technique and it is not recommended to depend solely to Basic Auth when designing the security features of your environment.

``` yaml
name: pipeline-reverse-proxy
kind: HTTPPipeline
flow:
- filter: oauth-validator
- filter: proxy
filters:
- kind: Validator
name: oauth-validator
basicAuth:
mode: "FILE"
userFile: '/etc/apache2/.htpasswd'
- name: proxy
kind: Proxy
```

* The example above uses credentials defined in `/etc/apache2/.htpasswd` to restrict access. Please check out apache2-utils documentation [7] for more details.

* For the full YAML, see [here](#basic-auth-1)

## References

### Header
Expand All @@ -167,7 +192,6 @@ filters:
- url: http://127.0.0.1:9097
loadBalance:
policy: roundRobin

```

### JWT
Expand Down Expand Up @@ -247,6 +271,30 @@ filters:
policy: roundRobin
```

### Basic Auth

``` yaml
name: pipeline-reverse-proxy
kind: HTTPPipeline
flow:
- filter: header-validator
- filter: proxy
filters:
- kind: Validator
name: basic-auth-validator
basicAuth:
mode: "FILE"
userFile: '/etc/apache2/.htpasswd'
- name: proxy
kind: Proxy
mainPool:
servers:
- url: http://127.0.0.1:9095
- url: http://127.0.0.1:9096
- url: http://127.0.0.1:9097
loadBalance:
policy: roundRobin
```

### Concepts

Expand All @@ -255,3 +303,5 @@ filters:
3. https://github.com/megaease/easegress/blob/main/doc/filters.md#signerliteral
4. https://oauth.net/2/
5. https://github.com/megaease/easegress/blob/main/doc/filters.md#validatorOAuth2JWT
6. https://en.wikipedia.org/wiki/Basic_access_authentication
7. https://manpages.debian.org/testing/apache2-utils/htpasswd.1.en.html
12 changes: 11 additions & 1 deletion doc/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ The filter always returns an empty result.

## Validator

The Validator filter validates requests, forwards valid ones, and rejects invalid ones. Four validation methods (`headers`, `jwt`, `signature`, and `oauth2`) are supported up to now, and these methods can either be used together or alone. When two or more methods are used together, a request needs to pass all of them to be forwarded.
The Validator filter validates requests, forwards valid ones, and rejects invalid ones. Four validation methods (`headers`, `jwt`, `signature`, `oauth2` and `basicAuth`) are supported up to now, and these methods can either be used together or alone. When two or more methods are used together, a request needs to pass all of them to be forwarded.

Below is an example configuration for the `headers` validation method. Requests which has a header named `Is-Valid` with value `abc` or `goodplan` or matches regular expression `^ok-.+$` are considered to be valid.

Expand Down Expand Up @@ -592,6 +592,15 @@ oauth2:
insecureTls: false
```

Here's an example for `basicAuth` validation method which uses [Apache2 htpasswd](https://manpages.debian.org/testing/apache2-utils/htpasswd.1.en.html) formatted encrypted password file for validation.
```yaml
kind: Validator
name: basicAuth-validator-example
basicAuth:
mode: "FILE"
userFile: /etc/apache2/.htpasswd
```

### Configuration

| Name | Type | Description | Required |
Expand All @@ -600,6 +609,7 @@ oauth2:
| jwt | [validator.JWTValidatorSpec](#validatorJWTValidatorSpec) | JWT validation rule, validates JWT token string from the `Authorization` header or cookies | No |
| signature | [signer.Spec](#signerSpec) | Signature validation rule, implements an [Amazon Signature V4](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) compatible signature validation validator, with customizable literal strings | No |
| oauth2 | [validator.OAuth2ValidatorSpec](#validatorOAuth2ValidatorSpec) | The `OAuth/2` method support `Token Introspection` mode and `Self-Encoded Access Tokens` mode, only one mode can be configured at a time | No |
| basicAuth | [basicauth.BasicAuthValidatorSpec](#basicauthBasicAuthValidatorSpec) | The `BasicAuth` method support `FILE` mode and `ETCD` mode, only one mode can be configured at a time. | No |

### Results

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
github.com/fatih/color v1.12.0
github.com/fsnotify/fsnotify v1.4.9
github.com/ghodss/yaml v1.0.0
github.com/go-chi/chi/v5 v5.0.3
github.com/go-zookeeper/zk v1.0.2
Expand Down Expand Up @@ -51,6 +52,7 @@ require (
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/tcnksm/go-httpstat v0.2.1-0.20191008022543-e866bb274419
github.com/tg123/go-htpasswd v1.2.0
github.com/tidwall/gjson v1.11.0
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/valyala/fasttemplate v1.2.1
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20200415212048-7901bc822317/go.mod h1:DF8FZRxMHMGv/vP2lQP6h+dYzzjpuRn24VeRiYn3qjQ=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
Expand Down Expand Up @@ -1266,6 +1268,8 @@ github.com/tcnksm/go-httpstat v0.2.1-0.20191008022543-e866bb274419 h1:elOIj31UL4
github.com/tcnksm/go-httpstat v0.2.1-0.20191008022543-e866bb274419/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto=
github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM=
github.com/tidwall/gjson v1.11.0 h1:C16pk7tQNiH6VlCrtIXL1w8GaOsi1X3W8KDkE1BuYd4=
github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
Expand Down Expand Up @@ -1426,6 +1430,7 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
47 changes: 47 additions & 0 deletions pkg/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ package cluster

import (
"fmt"
"io/ioutil"
"os"
"sync"
"testing"
"time"

"github.com/phayes/freeport"
"go.etcd.io/etcd/api/v3/mvccpb"
"go.etcd.io/etcd/client/v3/concurrency"

"github.com/megaease/easegress/pkg/env"
"github.com/megaease/easegress/pkg/option"
)

func mockClusters(count int) []*cluster {
Expand Down Expand Up @@ -130,6 +136,28 @@ func closeClusters(clusters []*cluster) {
}
}

func createSecondaryNode(clusterName string, primaryListenPeerURLs []string) *cluster {
ports, err := freeport.GetFreePorts(1)
check(err)
name := fmt.Sprintf("secondary-member-x")
opt := option.New()
opt.Name = name
opt.ClusterName = clusterName
opt.ClusterRole = "secondary"
opt.ClusterRequestTimeout = "10s"
opt.Cluster.PrimaryListenPeerURLs = primaryListenPeerURLs
opt.APIAddr = fmt.Sprintf("localhost:%d", ports[0])

_, err = opt.Parse()
check(err)

env.InitServerDir(opt)

clusterInstance, err := New(opt)
check(err)
return clusterInstance.(*cluster)
}

func TestCluster(t *testing.T) {
t.Run("start cluster dynamically", func(t *testing.T) {
clusters := mockClusters(3)
Expand All @@ -139,7 +167,11 @@ func TestCluster(t *testing.T) {
})
t.Run("start static sized cluster", func(t *testing.T) {
clusterNodes := mockStaticCluster(3)
primaryName := clusterNodes[0].opt.ClusterName
primaryAddress := clusterNodes[0].opt.Cluster.InitialAdvertisePeerURLs
secondaryNode := createSecondaryNode(primaryName, primaryAddress)
defer closeClusters(clusterNodes)
defer closeClusters([]*cluster{secondaryNode})
})
}

Expand Down Expand Up @@ -501,3 +533,18 @@ func TestUtilEqual(t *testing.T) {
t.Error("isKeyValueEqual invalid, should equal")
}
}

func TestIsLeader(t *testing.T) {
etcdDirName, err := ioutil.TempDir("", "cluster-test")
check(err)
defer os.RemoveAll(etcdDirName)

clusterInstance := CreateClusterForTest(etcdDirName)
if !clusterInstance.IsLeader() {
t.Error("single node cluster should be leader")
}
wg := &sync.WaitGroup{}
wg.Add(1)
clusterInstance.CloseServer(wg)
wg.Wait()
}
64 changes: 64 additions & 0 deletions pkg/cluster/test_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2017, MegaEase
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cluster

import (
"fmt"

"github.com/phayes/freeport"

"github.com/megaease/easegress/pkg/env"
"github.com/megaease/easegress/pkg/option"
)

func check(e error) {
if e != nil {
panic(e)
}
}

// CreateClusterForTest creates a cluster for testing purposes.
func CreateClusterForTest(tempDir string) Cluster {
ports, err := freeport.GetFreePorts(3)
check(err)
name := fmt.Sprintf("test-member-x")
opt := option.New()
opt.Name = name
opt.ClusterName = "test-cluster"
opt.ClusterRole = "primary"
opt.ClusterRequestTimeout = "10s"
opt.Cluster.ListenClientURLs = []string{fmt.Sprintf("http://localhost:%d", ports[0])}
opt.Cluster.AdvertiseClientURLs = opt.Cluster.ListenClientURLs
opt.Cluster.ListenPeerURLs = []string{fmt.Sprintf("http://localhost:%d", ports[1])}
opt.Cluster.InitialAdvertisePeerURLs = opt.Cluster.ListenPeerURLs
opt.Cluster.InitialCluster = make(map[string]string)
opt.Cluster.InitialCluster[name] = opt.Cluster.InitialAdvertisePeerURLs[0]
opt.APIAddr = fmt.Sprintf("localhost:%d", ports[2])
opt.DataDir = fmt.Sprintf("%s/data", tempDir)
opt.LogDir = fmt.Sprintf("%s/log", tempDir)
opt.MemberDir = fmt.Sprintf("%s/member", tempDir)

_, err = opt.Parse()
check(err)

env.InitServerDir(opt)

clusterInstance, err := New(opt)
check(err)
return clusterInstance
}
Loading