Add forward authentication option

This commit is contained in:
Daniel Rampelt 2017-08-25 12:22:03 -04:00 committed by Traefiker
parent f16219f90a
commit 52b69fbcb8
11 changed files with 252 additions and 105 deletions

60
auth/forward.go Normal file
View file

@ -0,0 +1,60 @@
package auth
import (
"io/ioutil"
"net/http"
"github.com/containous/traefik/log"
"github.com/containous/traefik/types"
)
// Forward the authentication to a external server
func Forward(forward *types.Forward, w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
httpClient := http.Client{}
if forward.TLS != nil {
tlsConfig, err := forward.TLS.CreateTLSConfig()
if err != nil {
log.Debugf("Impossible to configure TLS to call %s. Cause %s", forward.Address, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
httpClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
forwardReq, err := http.NewRequest(http.MethodGet, forward.Address, nil)
if err != nil {
log.Debugf("Error calling %s. Cause %s", forward.Address, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
forwardReq.Header = r.Header
forwardResponse, forwardErr := httpClient.Do(forwardReq)
if forwardErr != nil {
log.Debugf("Error calling %s. Cause: %s", forward.Address, forwardErr)
w.WriteHeader(http.StatusInternalServerError)
return
}
body, readError := ioutil.ReadAll(forwardResponse.Body)
if readError != nil {
log.Debugf("Error reading body %s. Cause: %s", forward.Address, readError)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer forwardResponse.Body.Close()
if forwardResponse.StatusCode < http.StatusOK || forwardResponse.StatusCode >= http.StatusMultipleChoices {
log.Debugf("Remote error %s. StatusCode: %d", forward.Address, forwardResponse.StatusCode)
w.WriteHeader(forwardResponse.StatusCode)
w.Write(body)
return
}
r.RequestURI = r.URL.RequestURI()
next(w, r)
}

View file

@ -11,7 +11,7 @@ import (
"github.com/containous/staert" "github.com/containous/staert"
"github.com/containous/traefik/cluster" "github.com/containous/traefik/cluster"
"github.com/containous/traefik/integration/try" "github.com/containous/traefik/integration/try"
"github.com/containous/traefik/provider" "github.com/containous/traefik/types"
"github.com/docker/libkv" "github.com/docker/libkv"
"github.com/docker/libkv/store" "github.com/docker/libkv/store"
"github.com/docker/libkv/store/consul" "github.com/docker/libkv/store/consul"
@ -52,7 +52,7 @@ func (s *ConsulSuite) setupConsulTLS(c *check.C) {
s.composeProject.Start(c) s.composeProject.Start(c)
consul.Register() consul.Register()
clientTLS := &provider.ClientTLS{ clientTLS := &types.ClientTLS{
CA: "resources/tls/ca.cert", CA: "resources/tls/ca.cert",
Cert: "resources/tls/consul.cert", Cert: "resources/tls/consul.cert",
Key: "resources/tls/consul.key", Key: "resources/tls/consul.key",

View file

@ -6,7 +6,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/abbot/go-http-auth" goauth "github.com/abbot/go-http-auth"
"github.com/containous/traefik/auth"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/urfave/negroni" "github.com/urfave/negroni"
@ -30,7 +31,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
basicAuth := auth.NewBasicAuthenticator("traefik", authenticator.secretBasic) basicAuth := goauth.NewBasicAuthenticator("traefik", authenticator.secretBasic)
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if username := basicAuth.CheckAuth(r); username == "" { if username := basicAuth.CheckAuth(r); username == "" {
log.Debug("Basic auth failed...") log.Debug("Basic auth failed...")
@ -48,7 +49,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
digestAuth := auth.NewDigestAuthenticator("traefik", authenticator.secretDigest) digestAuth := goauth.NewDigestAuthenticator("traefik", authenticator.secretDigest)
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if username, _ := digestAuth.CheckAuth(r); username == "" { if username, _ := digestAuth.CheckAuth(r); username == "" {
log.Debug("Digest auth failed...") log.Debug("Digest auth failed...")
@ -61,6 +62,10 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
} }
}) })
} else if authConfig.Forward != nil {
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
auth.Forward(authConfig.Forward, w, r, next)
})
} }
return &authenticator, nil return &authenticator, nil
} }

View file

@ -186,3 +186,67 @@ func TestBasicAuthUserHeader(t *testing.T) {
assert.NoError(t, err, "there should be no error") assert.NoError(t, err, "there should be no error")
assert.Equal(t, "traefik\n", string(body), "they should be equal") assert.Equal(t, "traefik\n", string(body), "they should be equal")
} }
func TestForwardAuthFail(t *testing.T) {
authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Forbidden", http.StatusForbidden)
}))
defer authTs.Close()
authMiddleware, err := NewAuthenticator(&types.Auth{
Forward: &types.Forward{
Address: authTs.URL,
},
})
assert.NoError(t, err, "there should be no error")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(authMiddleware)
n.UseHandler(handler)
ts := httptest.NewServer(n)
defer ts.Close()
client := &http.Client{}
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, http.StatusForbidden, res.StatusCode, "they should be equal")
body, err := ioutil.ReadAll(res.Body)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, "Forbidden\n", string(body), "they should be equal")
}
func TestForwardAuthSuccess(t *testing.T) {
authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Success")
}))
defer authTs.Close()
authMiddleware, err := NewAuthenticator(&types.Auth{
Forward: &types.Forward{
Address: authTs.URL,
},
})
assert.NoError(t, err, "there should be no error")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(authMiddleware)
n.UseHandler(handler)
ts := httptest.NewServer(n)
defer ts.Close()
client := &http.Client{}
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, http.StatusOK, res.StatusCode, "they should be equal")
body, err := ioutil.ReadAll(res.Body)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, "traefik\n", string(body), "they should be equal")
}

View file

@ -47,12 +47,12 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
provider.BaseProvider `mapstructure:",squash"` provider.BaseProvider `mapstructure:",squash"`
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"` Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"`
Domain string `description:"Default domain used"` Domain string `description:"Default domain used"`
TLS *provider.ClientTLS `description:"Enable Docker TLS support"` TLS *types.ClientTLS `description:"Enable Docker TLS support"`
ExposedByDefault bool `description:"Expose containers by default"` ExposedByDefault bool `description:"Expose containers by default"`
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network"` UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network"`
SwarmMode bool `description:"Use Docker on Swarm Mode"` SwarmMode bool `description:"Use Docker on Swarm Mode"`
} }
// dockerData holds the need data to the Provider p // dockerData holds the need data to the Provider p

View file

@ -21,11 +21,11 @@ import (
// Provider holds common configurations of key-value providers. // Provider holds common configurations of key-value providers.
type Provider struct { type Provider struct {
provider.BaseProvider `mapstructure:",squash"` provider.BaseProvider `mapstructure:",squash"`
Endpoint string `description:"Comma separated server endpoints"` Endpoint string `description:"Comma separated server endpoints"`
Prefix string `description:"Prefix used for KV store"` Prefix string `description:"Prefix used for KV store"`
TLS *provider.ClientTLS `description:"Enable TLS support"` TLS *types.ClientTLS `description:"Enable TLS support"`
Username string `description:"KV Username"` Username string `description:"KV Username"`
Password string `description:"KV Password"` Password string `description:"KV Password"`
storeType store.Backend storeType store.Backend
kvclient store.Store kvclient store.Store
} }

View file

@ -53,18 +53,18 @@ var servicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P<service_name>.+
// Provider holds configuration of the provider. // Provider holds configuration of the provider.
type Provider struct { type Provider struct {
provider.BaseProvider provider.BaseProvider
Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon"` Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon"`
Domain string `description:"Default domain used"` Domain string `description:"Default domain used"`
ExposedByDefault bool `description:"Expose Marathon apps by default"` ExposedByDefault bool `description:"Expose Marathon apps by default"`
GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains"` GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains"`
DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header"` DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header"`
MarathonLBCompatibility bool `description:"Add compatibility with marathon-lb labels"` MarathonLBCompatibility bool `description:"Add compatibility with marathon-lb labels"`
TLS *provider.ClientTLS `description:"Enable Docker TLS support"` TLS *types.ClientTLS `description:"Enable Docker TLS support"`
DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"` DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"`
KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"` KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
ForceTaskHostname bool `description:"Force to use the task's hostname."` ForceTaskHostname bool `description:"Force to use the task's hostname."`
Basic *Basic `description:"Enable basic authentication"` Basic *Basic `description:"Enable basic authentication"`
RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments"` RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments"`
readyChecker *readinessChecker readyChecker *readinessChecker
marathonClient marathon.Marathon marathonClient marathon.Marathon
} }

View file

@ -2,11 +2,7 @@ package provider
import ( import (
"bytes" "bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil" "io/ioutil"
"os"
"strings" "strings"
"text/template" "text/template"
"unicode" "unicode"
@ -123,71 +119,3 @@ func ReverseStringSlice(slice *[]string) {
(*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i] (*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i]
} }
} }
// ClientTLS holds TLS specific configurations as client
// CA, Cert and Key can be either path or file contents
type ClientTLS struct {
CA string `description:"TLS CA"`
Cert string `description:"TLS cert"`
Key string `description:"TLS key"`
InsecureSkipVerify bool `description:"TLS insecure skip verify"`
}
// CreateTLSConfig creates a TLS config from ClientTLS structures
func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
var err error
if clientTLS == nil {
log.Warnf("clientTLS is nil")
return nil, nil
}
caPool := x509.NewCertPool()
if clientTLS.CA != "" {
var ca []byte
if _, errCA := os.Stat(clientTLS.CA); errCA == nil {
ca, err = ioutil.ReadFile(clientTLS.CA)
if err != nil {
return nil, fmt.Errorf("Failed to read CA. %s", err)
}
} else {
ca = []byte(clientTLS.CA)
}
caPool.AppendCertsFromPEM(ca)
}
cert := tls.Certificate{}
_, errKeyIsFile := os.Stat(clientTLS.Key)
if !clientTLS.InsecureSkipVerify && (len(clientTLS.Cert) == 0 || len(clientTLS.Key) == 0) {
return nil, fmt.Errorf("TLS Certificate or Key file must be set when TLS configuration is created")
}
if len(clientTLS.Cert) > 0 && len(clientTLS.Key) > 0 {
if _, errCertIsFile := os.Stat(clientTLS.Cert); errCertIsFile == nil {
if errKeyIsFile == nil {
cert, err = tls.LoadX509KeyPair(clientTLS.Cert, clientTLS.Key)
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls cert is a file, but tls key is not")
}
} else {
if errKeyIsFile != nil {
cert, err = tls.X509KeyPair([]byte(clientTLS.Cert), []byte(clientTLS.Key))
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls key is a file, but tls cert is not")
}
}
}
TLSConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
InsecureSkipVerify: clientTLS.InsecureSkipVerify,
}
return TLSConfig, nil
}

View file

@ -12,7 +12,7 @@ import (
type myProvider struct { type myProvider struct {
BaseProvider BaseProvider
TLS *ClientTLS TLS *types.ClientTLS
} }
func (p *myProvider) Foo() string { func (p *myProvider) Foo() string {
@ -202,7 +202,7 @@ func TestInsecureSkipVerifyClientTLS(t *testing.T) {
BaseProvider{ BaseProvider{
Filename: "", Filename: "",
}, },
&ClientTLS{ &types.ClientTLS{
InsecureSkipVerify: true, InsecureSkipVerify: true,
}, },
} }
@ -220,7 +220,7 @@ func TestInsecureSkipVerifyFalseClientTLS(t *testing.T) {
BaseProvider{ BaseProvider{
Filename: "", Filename: "",
}, },
&ClientTLS{ &types.ClientTLS{
InsecureSkipVerify: false, InsecureSkipVerify: false,
}, },
} }

View file

@ -304,6 +304,15 @@
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] # users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
# usersFile = "/path/to/.htdigest" # usersFile = "/path/to/.htdigest"
# #
# To enable forward auth on an entrypoint
# This configuration will first forward the request to http://authserver.com/auth. If the response code is 2XX,
# access is granted and the original request is performed. Otherwise, the response from the auth server is returned.
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.auth.forward]
# address = "http://authserver.com/auth"
#
# To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls): # To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls):
# [entryPoints] # [entryPoints]
# [entryPoints.https] # [entryPoints.https]
@ -714,11 +723,11 @@
# #
# keepAlive = "10s" # keepAlive = "10s"
# By default, a task's IP address (as returned by the Marathon API) is used as # By default, a task's IP address (as returned by the Marathon API) is used as
# backend server if an IP-per-task configuration can be found; otherwise, the # backend server if an IP-per-task configuration can be found; otherwise, the
# name of the host running the task is used. # name of the host running the task is used.
# The latter behavior can be enforced by enabling this switch. # The latter behavior can be enforced by enabling this switch.
# #
# Optional # Optional
# Default: false # Default: false
# #

View file

@ -7,6 +7,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
"github.com/containous/traefik/log"
"github.com/docker/libkv/store" "github.com/docker/libkv/store"
"github.com/ryanuber/go-glob" "github.com/ryanuber/go-glob"
) )
@ -299,6 +305,7 @@ type Cluster struct {
type Auth struct { type Auth struct {
Basic *Basic Basic *Basic
Digest *Digest Digest *Digest
Forward *Forward
HeaderField string HeaderField string
} }
@ -317,6 +324,12 @@ type Digest struct {
UsersFile string UsersFile string
} }
// Forward authentication
type Forward struct {
Address string `description:"Authentication server address"`
TLS *ClientTLS `description:"Enable TLS support"`
}
// CanonicalDomain returns a lower case domain with trim space // CanonicalDomain returns a lower case domain with trim space
func CanonicalDomain(domain string) string { func CanonicalDomain(domain string) string {
return strings.ToLower(strings.TrimSpace(domain)) return strings.ToLower(strings.TrimSpace(domain))
@ -388,3 +401,71 @@ type AccessLog struct {
FilePath string `json:"file,omitempty" description:"Access log file path. Stdout is used when omitted or empty"` FilePath string `json:"file,omitempty" description:"Access log file path. Stdout is used when omitted or empty"`
Format string `json:"format,omitempty" description:"Access log format: json | common"` Format string `json:"format,omitempty" description:"Access log format: json | common"`
} }
// ClientTLS holds TLS specific configurations as client
// CA, Cert and Key can be either path or file contents
type ClientTLS struct {
CA string `description:"TLS CA"`
Cert string `description:"TLS cert"`
Key string `description:"TLS key"`
InsecureSkipVerify bool `description:"TLS insecure skip verify"`
}
// CreateTLSConfig creates a TLS config from ClientTLS structures
func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) {
var err error
if clientTLS == nil {
log.Warnf("clientTLS is nil")
return nil, nil
}
caPool := x509.NewCertPool()
if clientTLS.CA != "" {
var ca []byte
if _, errCA := os.Stat(clientTLS.CA); errCA == nil {
ca, err = ioutil.ReadFile(clientTLS.CA)
if err != nil {
return nil, fmt.Errorf("Failed to read CA. %s", err)
}
} else {
ca = []byte(clientTLS.CA)
}
caPool.AppendCertsFromPEM(ca)
}
cert := tls.Certificate{}
_, errKeyIsFile := os.Stat(clientTLS.Key)
if !clientTLS.InsecureSkipVerify && (len(clientTLS.Cert) == 0 || len(clientTLS.Key) == 0) {
return nil, fmt.Errorf("TLS Certificate or Key file must be set when TLS configuration is created")
}
if len(clientTLS.Cert) > 0 && len(clientTLS.Key) > 0 {
if _, errCertIsFile := os.Stat(clientTLS.Cert); errCertIsFile == nil {
if errKeyIsFile == nil {
cert, err = tls.LoadX509KeyPair(clientTLS.Cert, clientTLS.Key)
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls cert is a file, but tls key is not")
}
} else {
if errKeyIsFile != nil {
cert, err = tls.X509KeyPair([]byte(clientTLS.Cert), []byte(clientTLS.Key))
if err != nil {
return nil, fmt.Errorf("Failed to load TLS keypair: %v", err)
}
} else {
return nil, fmt.Errorf("tls key is a file, but tls cert is not")
}
}
}
TLSConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
InsecureSkipVerify: clientTLS.InsecureSkipVerify,
}
return TLSConfig, nil
}