-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3825151
Showing
38 changed files
with
3,043 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
displayName: Ip filter or Basic auth | ||
type: middleware | ||
|
||
import: github.com/alexandreh2ag/traefik-ipfilter-basicauth | ||
|
||
summary: 'Restricts access to your services by ip whitelist or basic auth' | ||
|
||
testData: | ||
basicAuth: | ||
realm: "Realm" | ||
users: | ||
- traefik:$apr1$imy7rq16$PbXJYj5lsqZ71HoIBfm/T0 # traefik / traefik | ||
ipWhiteList: | ||
sourceRange: | ||
- "127.0.0.1" | ||
- "10.0.0.1/32" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Ip Filter - Basic auth | ||
|
||
Ip Filter - Basic auth is a middleware plugin for [Traefik](https://github.com/traefik/traefik) which try to authorize client by IP address or at least by Basic auth. | ||
|
||
## Configuration | ||
|
||
### Static | ||
|
||
```toml | ||
[pilot] | ||
token = "xxxx" | ||
|
||
[experimental.plugins.ipFilter_basicAuth] | ||
modulename = "github.com/alexandreh2ag/traefik-ipfilter-basicauth" | ||
version = "vX.X.X" | ||
``` | ||
|
||
### Dynamic | ||
|
||
To configure the `Ip Filter - Basic auth` plugin you should create a [middleware](https://doc.traefik.io/traefik/middlewares/overview/) in | ||
your dynamic configuration as explained [here](https://doc.traefik.io/traefik/middlewares/overview/). | ||
|
||
You must define at least one source range IP and one user from configuration or file. | ||
|
||
The configuration of middleware is quite similar then traefik middlewares ([IPWhiteList](https://doc.traefik.io/traefik/middlewares/http/ipwhitelist/) / [BasicAuth](https://doc.traefik.io/traefik/middlewares/http/basicauth/)). | ||
|
||
```yaml | ||
http: | ||
middlewares: | ||
my-ipFilter_basicAuth: | ||
plugin: | ||
ipFilter_basicAuth: | ||
basicAuth: | ||
realm: "Realm" | ||
usersFile: "/path/to/my/usersfile" | ||
users: | ||
- traefik:$apr1$imy7rq16$PbXJYj5lsqZ71HoIBfm/T0 # traefik / traefik | ||
headerField: "X-WebAuth-User" | ||
removeHeader: true | ||
ipWhiteList: | ||
sourceRange: | ||
- "127.0.0.1" | ||
- "10.0.0.1/32" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package traefik_ipfilter_basicauth | ||
|
||
import ( | ||
"os" | ||
"strings" | ||
) | ||
|
||
// UserParser Parses a string and return a userName/userHash. An error if the format of the string is incorrect. | ||
type UserParser func(user string) (string, string, error) | ||
|
||
const ( | ||
defaultRealm = "traefik" | ||
authorizationHeader = "Authorization" | ||
) | ||
|
||
func getUsers(fileName string, appendUsers []string, parser UserParser) (map[string]string, error) { | ||
users, err := loadUsers(fileName, appendUsers) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
userMap := make(map[string]string) | ||
for _, user := range users { | ||
userName, userHash, err := parser(user) | ||
if err != nil { | ||
return nil, err | ||
} | ||
userMap[userName] = userHash | ||
} | ||
|
||
return userMap, nil | ||
} | ||
|
||
func loadUsers(fileName string, appendUsers []string) ([]string, error) { | ||
var users []string | ||
var err error | ||
|
||
if fileName != "" { | ||
users, err = getLinesFromFile(fileName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return append(users, appendUsers...), nil | ||
} | ||
|
||
func getLinesFromFile(filename string) ([]string, error) { | ||
dat, err := os.ReadFile(filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Trim lines and filter out blanks | ||
rawLines := strings.Split(string(dat), "\n") | ||
var filteredLines []string | ||
for _, rawLine := range rawLines { | ||
line := strings.TrimSpace(rawLine) | ||
if line != "" && !strings.HasPrefix(line, "#") { | ||
filteredLines = append(filteredLines, line) | ||
} | ||
} | ||
|
||
return filteredLines, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package traefik_ipfilter_basicauth | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net" | ||
"strings" | ||
) | ||
|
||
// Checker allows to check that addresses are in a trusted IPs. | ||
type Checker struct { | ||
authorizedIPs []*net.IP | ||
authorizedIPsNet []*net.IPNet | ||
} | ||
|
||
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs. | ||
func NewChecker(trustedIPs []string) (*Checker, error) { | ||
if len(trustedIPs) == 0 { | ||
return nil, errors.New("no trusted IPs provided") | ||
} | ||
|
||
checker := &Checker{} | ||
|
||
for _, ipMask := range trustedIPs { | ||
if ipAddr := net.ParseIP(ipMask); ipAddr != nil { | ||
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr) | ||
continue | ||
} | ||
|
||
_, ipAddr, err := net.ParseCIDR(ipMask) | ||
if err != nil { | ||
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err) | ||
} | ||
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr) | ||
} | ||
|
||
return checker, nil | ||
} | ||
|
||
// IsAuthorized checks if provided request is authorized by the trusted IPs. | ||
func (ip *Checker) IsAuthorized(addr string) error { | ||
var invalidMatches []string | ||
|
||
host, _, err := net.SplitHostPort(addr) | ||
if err != nil { | ||
host = addr | ||
} | ||
|
||
ok, err := ip.Contains(host) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !ok { | ||
invalidMatches = append(invalidMatches, addr) | ||
return fmt.Errorf("%q matched none of the trusted IPs", strings.Join(invalidMatches, ", ")) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Contains checks if provided address is in the trusted IPs. | ||
func (ip *Checker) Contains(addr string) (bool, error) { | ||
if len(addr) == 0 { | ||
return false, errors.New("empty IP address") | ||
} | ||
|
||
ipAddr, err := parseIP(addr) | ||
if err != nil { | ||
return false, fmt.Errorf("unable to parse address: %s: %w", addr, err) | ||
} | ||
|
||
return ip.ContainsIP(ipAddr), nil | ||
} | ||
|
||
// ContainsIP checks if provided address is in the trusted IPs. | ||
func (ip *Checker) ContainsIP(addr net.IP) bool { | ||
for _, authorizedIP := range ip.authorizedIPs { | ||
fmt.Println("Client IP: " + addr.String() + " - ip check : " + authorizedIP.String()) | ||
if authorizedIP.Equal(addr) { | ||
return true | ||
} | ||
} | ||
|
||
for _, authorizedNet := range ip.authorizedIPsNet { | ||
if authorizedNet.Contains(addr) { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
func parseIP(addr string) (net.IP, error) { | ||
userIP := net.ParseIP(addr) | ||
if userIP == nil { | ||
return nil, fmt.Errorf("can't parse IP from address %s", addr) | ||
} | ||
|
||
return userIP, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module github.com/alexandreh2ag/traefik-ipfilter-basicauth | ||
|
||
go 1.17 | ||
|
||
require ( | ||
github.com/abbot/go-http-auth v0.4.0 // indirect | ||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect | ||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= | ||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= | ||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= | ||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 h1:0qjDla5xICC2suMtyRH/QqX3B1btXTfNsIt/i4LFgO0= | ||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package traefik_ipfilter_basicauth | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
goauth "github.com/abbot/go-http-auth" | ||
) | ||
|
||
type BasicAuth struct { | ||
Users []string `json:"users,omitempty" toml:"users,omitempty" yaml:"users,omitempty" loggable:"false"` | ||
UsersFile string `json:"usersFile,omitempty" toml:"usersFile,omitempty" yaml:"usersFile,omitempty"` | ||
Realm string `json:"realm,omitempty" toml:"realm,omitempty" yaml:"realm,omitempty"` | ||
RemoveHeader bool `json:"removeHeader,omitempty" toml:"removeHeader,omitempty" yaml:"removeHeader,omitempty" export:"true"` | ||
HeaderField string `json:"headerField,omitempty" toml:"headerField,omitempty" yaml:"headerField,omitempty" export:"true"` | ||
} | ||
|
||
type IPWhiteList struct { | ||
SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"` | ||
} | ||
|
||
// Config the plugin configuration. | ||
type Config struct { | ||
BasicAuth BasicAuth `json:"basicAuth,omitempty" toml:"basicAuth,omitempty" yaml:"basicAuth,omitempty"` | ||
IPWhiteList IPWhiteList `json:"ipWhiteList,omitempty" toml:"ipWhiteList,omitempty" yaml:"ipWhiteList,omitempty"` | ||
} | ||
|
||
// CreateConfig creates the default plugin configuration. | ||
func CreateConfig() *Config { | ||
return &Config{} | ||
} | ||
|
||
// Middleware a Middleware plugin. | ||
type Middleware struct { | ||
auth *goauth.BasicAuth | ||
next http.Handler | ||
users map[string]string | ||
headerField string | ||
removeHeader bool | ||
whiteLister *Checker | ||
name string | ||
} | ||
|
||
// New created a new Middleware plugin. | ||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { | ||
if len(config.IPWhiteList.SourceRange) == 0 { | ||
return nil, errors.New("sourceRange is empty, IPWhiteLister not created") | ||
} | ||
|
||
checker, err := NewChecker(config.IPWhiteList.SourceRange) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse CIDR whitelist %s: %w", config.IPWhiteList.SourceRange, err) | ||
} | ||
|
||
users, err := getUsers(config.BasicAuth.UsersFile, config.BasicAuth.Users, basicUserParser) | ||
if err != nil { | ||
return nil, err | ||
} | ||
m := &Middleware{ | ||
users: users, | ||
whiteLister: checker, | ||
removeHeader: config.BasicAuth.RemoveHeader, | ||
headerField: config.BasicAuth.HeaderField, | ||
next: next, | ||
name: name, | ||
} | ||
realm := defaultRealm | ||
if len(config.BasicAuth.Realm) > 0 { | ||
realm = config.BasicAuth.Realm | ||
} | ||
m.auth = &goauth.BasicAuth{Realm: realm, Secrets: m.secretBasic} | ||
|
||
return m, nil | ||
} | ||
|
||
func (m *Middleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||
user := "" | ||
if req.RemoteAddr == "" { | ||
fmt.Println("RemoteAddr is empty") | ||
return | ||
} | ||
|
||
err := m.whiteLister.IsAuthorized(req.RemoteAddr) | ||
if err != nil { | ||
fmt.Println("Try basic auth") | ||
ok := false | ||
if user = m.auth.CheckAuth(req); user == "" { | ||
ok = false | ||
} else { | ||
ok = true | ||
} | ||
|
||
if !ok { | ||
m.auth.RequireAuth(rw, req) | ||
fmt.Println("IP is not authorize and basic auth is not valid") | ||
return | ||
} | ||
} | ||
req.URL.User = url.User(user) | ||
if m.headerField != "" { | ||
req.Header[m.headerField] = []string{user} | ||
} | ||
|
||
if m.removeHeader { | ||
req.Header.Del(authorizationHeader) | ||
} | ||
|
||
fmt.Println("Request authorized") | ||
m.next.ServeHTTP(rw, req) | ||
} | ||
|
||
func (m *Middleware) secretBasic(user, realm string) string { | ||
if secret, ok := m.users[user]; ok { | ||
return secret | ||
} | ||
|
||
return "" | ||
} | ||
|
||
func basicUserParser(user string) (string, string, error) { | ||
split := strings.Split(user, ":") | ||
if len(split) != 2 { | ||
return "", "", fmt.Errorf("error parsing BasicUser: %v", user) | ||
} | ||
return split[0], split[1], nil | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.