From bb14ec70bd47538440de92088121e2c7b4d24144 Mon Sep 17 00:00:00 2001 From: Mikael Rapp Date: Mon, 2 Jul 2018 11:52:04 +0200 Subject: [PATCH] Auth support in frontends for k8s and file --- autogen/gentemplates/gen.go | 35 ++ docs/configuration/backends/file.md | 27 ++ docs/configuration/backends/kubernetes.md | 54 +-- docs/configuration/entrypoints.md | 47 ++- docs/user-guide/examples.md | 36 -- provider/kubernetes/annotations.go | 60 +-- .../kubernetes/builder_configuration_test.go | 47 ++- provider/kubernetes/kubernetes.go | 274 +++++++++---- provider/kubernetes/kubernetes_test.go | 374 +++++++++++++++++- server/server_middlewares.go | 11 + server/server_middlewares_test.go | 37 ++ templates/kubernetes.tmpl | 35 ++ testhelpers/config.go | 8 + types/types.go | 3 +- 14 files changed, 867 insertions(+), 181 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 98f004b5f..40eb18681 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1103,6 +1103,41 @@ var _templatesKubernetesTmpl = []byte(`[backends] "{{.}}", {{end}}] + {{if $frontend.Auth }} + [frontends."{{ $frontendName }}".auth] + headerField = "X-WebAuth-User" + + {{if $frontend.Auth.Basic }} + [frontends."{{ $frontendName }}".auth.basic] + users = [{{range $frontend.Auth.Basic.Users }} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Auth.Digest }} + [frontends."{{ $frontendName }}".auth.digest] + users = [{{range $frontend.Auth.Digest.Users }} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Auth.Forward }} + [frontends."{{ $frontendName }}".auth.forward] + address = "{{ $frontend.Auth.Forward.Address }}" + authResponseHeaders = [{{range $frontend.Auth.Forward.AuthResponseHeaders }} + "{{.}}", + {{end}}] + trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} + {{if $frontend.Auth.Forward.TLS }} + [frontends."{{ $frontendName }}".auth.forward.tls] + cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" + key = "{{ $frontend.Auth.Forward.TLS.Key }}" + insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} + {{end}} + {{end}} + + {{end}} + {{if $frontend.WhiteList }} [frontends."{{ $frontendName }}".whiteList] sourceRange = [{{range $frontend.WhiteList.SourceRange }} diff --git a/docs/configuration/backends/file.md b/docs/configuration/backends/file.md index c8050c5a4..e4a4bed2b 100644 --- a/docs/configuration/backends/file.md +++ b/docs/configuration/backends/file.md @@ -55,11 +55,38 @@ Træfik can be configured with a file. passHostHeader = true passTLSCert = true priority = 42 + + # Use frontends.frontend1.auth.basic below instead basicAuth = [ "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", ] + [frontends.frontend1.auth] + headerField = "X-WebAuth-User" + [frontends.frontend1.auth.basic] + users = [ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + ] + usersFile = "/path/to/.htpasswd" + [frontends.frontend1.auth.digest] + users = [ + "test:traefik:a2688e031edb4be6a3797f3882655c05", + "test2:traefik:518845800f9e2bfb1f1f740ec24f074e", + ] + usersFile = "/path/to/.htdigest" + [frontends.frontend1.auth.forward] + address = "https://authserver.com/auth" + trustForwardHeader = true + authResponseHeaders = ["X-Auth-User"] + [frontends.frontend1.auth.forward.tls] + ca = [ "path/to/local.crt"] + caOptional = true + cert = "path/to/foo.cert" + key = "path/to/foo.key" + insecureSkipVerify = true + [frontends.frontend1.whiteList] sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"] useXForwardedFor = true diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 621ec1740..43186f62e 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -140,25 +140,25 @@ If the service port defined in the ingress spec is 443, then the backend communi The following general annotations are applicable on the Ingress object: -| Annotation | Description | -|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik.ingress.kubernetes.io/buffering: ` | (3) See [buffering](/configuration/commons/#buffering) section. | -| `traefik.ingress.kubernetes.io/error-pages: ` | (1) See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik.ingress.kubernetes.io/frontend-entry-points: http,https` | Override the default frontend endpoints. | -| `traefik.ingress.kubernetes.io/pass-tls-cert: "true"` | Override the default frontend PassTLSCert value. Default: `false`. | -| `traefik.ingress.kubernetes.io/preserve-host: "true"` | Forward client `Host` header to the backend. | -| `traefik.ingress.kubernetes.io/priority: "3"` | Override the default frontend rule priority. | -| `traefik.ingress.kubernetes.io/rate-limit: ` | (2) See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik.ingress.kubernetes.io/redirect-entry-point: https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). | -| `traefik.ingress.kubernetes.io/redirect-permanent: "true"` | Return 301 instead of 302. | -| `traefik.ingress.kubernetes.io/redirect-regex: ^http://localhost/(.*)` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-replacement`. | -| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. | -| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | -| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. | -| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (6). | -| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. | -| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) | -| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) | +| Annotation | Description | +|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| `traefik.ingress.kubernetes.io/buffering: ` | (3) See [buffering](/configuration/commons/#buffering) section. | +| `traefik.ingress.kubernetes.io/error-pages: ` | (1) See [custom error pages](/configuration/commons/#custom-error-pages) section. | +| `traefik.ingress.kubernetes.io/frontend-entry-points: http,https` | Override the default frontend endpoints. | +| `traefik.ingress.kubernetes.io/pass-tls-cert: "true"` | Override the default frontend PassTLSCert value. Default: `false`. | +| `traefik.ingress.kubernetes.io/preserve-host: "true"` | Forward client `Host` header to the backend. | +| `traefik.ingress.kubernetes.io/priority: "3"` | Override the default frontend rule priority. | +| `traefik.ingress.kubernetes.io/rate-limit: ` | (2) See [rate limiting](/configuration/commons/#rate-limiting) section. | +| `traefik.ingress.kubernetes.io/redirect-entry-point: https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). | +| `traefik.ingress.kubernetes.io/redirect-permanent: "true"` | Return 301 instead of 302. | +| `traefik.ingress.kubernetes.io/redirect-regex: ^http://localhost/(.*)` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-replacement`. | +| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. | +| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | +| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. | +| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (6). | +| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. | +| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) | +| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) | <1> `traefik.ingress.kubernetes.io/error-pages` example: @@ -297,14 +297,20 @@ The following security annotations are applicable on the Ingress object: Additional authentication annotations can be added to the Ingress object. The source of the authentication is a Secret object that contains the credentials. -| Annotation | Description | -|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| `ingress.kubernetes.io/auth-type: basic` | Contains the authentication type. The only permitted type is `basic`. | -| `ingress.kubernetes.io/auth-secret: mysecret` | Name of Secret containing the username and password with access to the paths defined in the Ingress object. | +| Annotation | basic | digest | forward | Description | +|----------------------------------------------------------------------|-------|--------|---------|-------------------------------------------------------------------------------------------------------------| +| `ingress.kubernetes.io/auth-type: basic` | x | x | x | Contains the authentication type: `basic`, `digest`, `forward`. | +| `ingress.kubernetes.io/auth-secret: mysecret` | x | x | | Name of Secret containing the username and password with access to the paths defined in the Ingress object. | +| `ingress.kubernetes.io/auth-header-field: X-WebAuth-User` | x | x | | Pass Authenticated user to application via headers. | +| `ingress.kubernetes.io/auth-url: https://example.com` | | | x | [The URL of the authentication server](configuration/entrypoints/#forward-authentication). | +| `ingress.kubernetes.io/auth-trust-headers: false` | | | x | Trust `X-Forwarded-*` headers. | +| `ingress.kubernetes.io/auth-response-headers: X-Auth-User, X-Secret` | | | x | Copy headers from the authentication server to the request. | +| `ingress.kubernetes.io/auth-tls-secret: secret` | | | x | Name of Secret containing the certificate and key for the forward auth. | +| `ingress.kubernetes.io/auth-tls-insecure` | | | x | If set to `true` invalid SSL certificates are accepted. | The secret must be created in the same namespace as the Ingress object. -The following limitations hold: +The following limitations hold for basic/digest auth: - The realm is not configurable; the only supported (and default) value is `traefik`. - The Secret must contain a single file only. diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index ff36c6231..c217d35ef 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -54,14 +54,13 @@ [entryPoints.http.auth.forward] address = "https://authserver.com/auth" trustForwardHeader = true + authResponseHeaders = ["X-Auth-User"] [entryPoints.http.auth.forward.tls] ca = [ "path/to/local.crt"] caOptional = true cert = "path/to/foo.cert" key = "path/to/foo.key" insecureSkipVerify = true - [entryPoints.http.auth.forward] - authResponseHeaders = ["X-Auth-User"] [entryPoints.http.proxyProtocol] insecure = true @@ -273,6 +272,18 @@ Users can be specified directly in the TOML file, or indirectly by referencing a usersFile = "/path/to/.htpasswd" ``` +Optionally, you can pass authenticated user to application via headers + +```toml +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.auth] + headerField = "X-WebAuth-User" # <-- + [entryPoints.http.auth.basic] + users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +``` + ### Digest Authentication You can use `htdigest` to generate them. @@ -290,6 +301,18 @@ Users can be specified directly in the TOML file, or indirectly by referencing a usersFile = "/path/to/.htdigest" ``` +Optionally, you can pass authenticated user to application via headers + +```toml +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.auth] + headerField = "X-WebAuth-User" # <-- + [entryPoints.http.auth.digest] + users = ["test:traefik:a2688e031edb4be6a3797f3882655c05", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] +``` + ### Forward Authentication This configuration will first forward the request to `http://authserver.com/auth`. @@ -313,17 +336,21 @@ Otherwise, the response from the authentication server is returned. # trustForwardHeader = true - # Copy headers from the authentication server to the request - [entryPoints.http.auth.forward] - authResponseHeaders = ["X-Auth-User", "X-Secret"] - - # Enable forward auth TLS connection. + # Copy headers from the authentication server to the request. # # Optional # - [entryPoints.http.auth.forward.tls] - cert = "authserver.crt" - key = "authserver.key" + authResponseHeaders = ["X-Auth-User", "X-Secret"] + + # Enable forward auth TLS connection. + # + # Optional + # + [entryPoints.http.auth.forward.tls] + ca = [ "path/to/local.crt"] + caOptional = true + cert = "path/to/foo.cert" + key = "path/to/foo.key" ``` ## Specify Minimum TLS Version diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index a26b5029c..70a6f5895 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -322,42 +322,6 @@ The `consul` provider contains the configuration. rule = "Path:/test" ``` -## Enable Basic authentication in an entry point - -With two user/pass: - -- `test`:`test` -- `test2`:`test2` - -Passwords are encoded in MD5: you can use `htpasswd` to generate them. - -```toml -defaultEntryPoints = ["http"] - -[entryPoints] - [entryPoints.http] - address = ":80" - [entryPoints.http.auth.basic] - users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] -``` - -## Pass Authenticated user to application via headers - -Providing an authentication method as described above, it is possible to pass the user to the application -via a configurable header value. - -```toml -defaultEntryPoints = ["http"] - -[entryPoints] - [entryPoints.http] - address = ":80" - [entryPoints.http.auth] - headerField = "X-WebAuth-User" - [entryPoints.http.auth.basic] - users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] -``` - ## Override the Traefik HTTP server idleTimeout and/or throttle configurations from re-loading too quickly ```toml diff --git a/provider/kubernetes/annotations.go b/provider/kubernetes/annotations.go index 5feec1598..a12b25061 100644 --- a/provider/kubernetes/annotations.go +++ b/provider/kubernetes/annotations.go @@ -5,33 +5,39 @@ import ( ) const ( - annotationKubernetesIngressClass = "kubernetes.io/ingress.class" - annotationKubernetesAuthRealm = "ingress.kubernetes.io/auth-realm" - annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type" - annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret" - annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target" - annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range" - annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for" - annotationKubernetesPreserveHost = "ingress.kubernetes.io/preserve-host" - annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert" - annotationKubernetesFrontendEntryPoints = "ingress.kubernetes.io/frontend-entry-points" - annotationKubernetesPriority = "ingress.kubernetes.io/priority" - annotationKubernetesCircuitBreakerExpression = "ingress.kubernetes.io/circuit-breaker-expression" - annotationKubernetesLoadBalancerMethod = "ingress.kubernetes.io/load-balancer-method" - annotationKubernetesAffinity = "ingress.kubernetes.io/affinity" - annotationKubernetesSessionCookieName = "ingress.kubernetes.io/session-cookie-name" - annotationKubernetesRuleType = "ingress.kubernetes.io/rule-type" - annotationKubernetesRedirectEntryPoint = "ingress.kubernetes.io/redirect-entry-point" - annotationKubernetesRedirectPermanent = "ingress.kubernetes.io/redirect-permanent" - annotationKubernetesRedirectRegex = "ingress.kubernetes.io/redirect-regex" - annotationKubernetesRedirectReplacement = "ingress.kubernetes.io/redirect-replacement" - annotationKubernetesMaxConnAmount = "ingress.kubernetes.io/max-conn-amount" - annotationKubernetesMaxConnExtractorFunc = "ingress.kubernetes.io/max-conn-extractor-func" - annotationKubernetesRateLimit = "ingress.kubernetes.io/rate-limit" - annotationKubernetesErrorPages = "ingress.kubernetes.io/error-pages" - annotationKubernetesBuffering = "ingress.kubernetes.io/buffering" - annotationKubernetesAppRoot = "ingress.kubernetes.io/app-root" - annotationKubernetesServiceWeights = "ingress.kubernetes.io/service-weights" + annotationKubernetesIngressClass = "kubernetes.io/ingress.class" + annotationKubernetesAuthRealm = "ingress.kubernetes.io/auth-realm" + annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type" + annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret" + annotationKubernetesAuthHeaderField = "ingress.kubernetes.io/auth-header-field" + annotationKubernetesAuthForwardResponseHeaders = "ingress.kubernetes.io/auth-response-headers" + annotationKubernetesAuthForwardURL = "ingress.kubernetes.io/auth-url" + annotationKubernetesAuthForwardTrustHeaders = "ingress.kubernetes.io/auth-trust-headers" + annotationKubernetesAuthForwardTLSSecret = "ingress.kubernetes.io/auth-tls-secret" + annotationKubernetesAuthForwardTLSInsecure = "ingress.kubernetes.io/auth-tls-insecure" + annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target" + annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range" + annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for" + annotationKubernetesPreserveHost = "ingress.kubernetes.io/preserve-host" + annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert" + annotationKubernetesFrontendEntryPoints = "ingress.kubernetes.io/frontend-entry-points" + annotationKubernetesPriority = "ingress.kubernetes.io/priority" + annotationKubernetesCircuitBreakerExpression = "ingress.kubernetes.io/circuit-breaker-expression" + annotationKubernetesLoadBalancerMethod = "ingress.kubernetes.io/load-balancer-method" + annotationKubernetesAffinity = "ingress.kubernetes.io/affinity" + annotationKubernetesSessionCookieName = "ingress.kubernetes.io/session-cookie-name" + annotationKubernetesRuleType = "ingress.kubernetes.io/rule-type" + annotationKubernetesRedirectEntryPoint = "ingress.kubernetes.io/redirect-entry-point" + annotationKubernetesRedirectPermanent = "ingress.kubernetes.io/redirect-permanent" + annotationKubernetesRedirectRegex = "ingress.kubernetes.io/redirect-regex" + annotationKubernetesRedirectReplacement = "ingress.kubernetes.io/redirect-replacement" + annotationKubernetesMaxConnAmount = "ingress.kubernetes.io/max-conn-amount" + annotationKubernetesMaxConnExtractorFunc = "ingress.kubernetes.io/max-conn-extractor-func" + annotationKubernetesRateLimit = "ingress.kubernetes.io/rate-limit" + annotationKubernetesErrorPages = "ingress.kubernetes.io/error-pages" + annotationKubernetesBuffering = "ingress.kubernetes.io/buffering" + annotationKubernetesAppRoot = "ingress.kubernetes.io/app-root" + annotationKubernetesServiceWeights = "ingress.kubernetes.io/service-weights" annotationKubernetesSSLForceHost = "ingress.kubernetes.io/ssl-force-host" annotationKubernetesSSLRedirect = "ingress.kubernetes.io/ssl-redirect" diff --git a/provider/kubernetes/builder_configuration_test.go b/provider/kubernetes/builder_configuration_test.go index 81ec0f08a..ccb82505c 100644 --- a/provider/kubernetes/builder_configuration_test.go +++ b/provider/kubernetes/builder_configuration_test.go @@ -208,9 +208,52 @@ func entryPoints(eps ...string) func(*types.Frontend) { } } -func basicAuth(auth ...string) func(*types.Frontend) { +// Deprecated +func basicAuthDeprecated(auth ...string) func(*types.Frontend) { return func(f *types.Frontend) { - f.BasicAuth = auth + f.Auth = &types.Auth{Basic: &types.Basic{Users: auth}} + } +} + +func auth(opt func(*types.Auth)) func(*types.Frontend) { + return func(f *types.Frontend) { + auth := &types.Auth{} + opt(auth) + f.Auth = auth + } +} + +func basicAuth(users ...string) func(*types.Auth) { + return func(a *types.Auth) { + a.Basic = &types.Basic{Users: users} + } +} + +func forwardAuth(forwardURL string, opts ...func(*types.Forward)) func(*types.Auth) { + return func(a *types.Auth) { + fwd := &types.Forward{Address: forwardURL} + for _, opt := range opts { + opt(fwd) + } + a.Forward = fwd + } +} + +func fwdAuthResponseHeaders(headers ...string) func(*types.Forward) { + return func(f *types.Forward) { + f.AuthResponseHeaders = headers + } +} + +func fwdTrustForwardHeader() func(*types.Forward) { + return func(f *types.Forward) { + f.TrustForwardHeader = true + } +} + +func fwdAuthTLS(cert, key string, insecure bool) func(*types.Forward) { + return func(f *types.Forward) { + f.TLS = &types.ClientTLS{Cert: cert, Key: key, InsecureSkipVerify: insecure} } } diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d92b0fc5e..3d38a623a 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -221,9 +221,9 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } if _, exists := templateObjects.Frontends[baseName]; !exists { - basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) + auth, err := getAuthConfig(i, k8sClient) if err != nil { - log.Errorf("Failed to retrieve basic auth configuration for ingress %s/%s: %s", i.Namespace, i.Name, err) + log.Errorf("Failed to retrieve auth configuration for ingress %s/%s: %s", i.Namespace, i.Name, err) continue } @@ -238,13 +238,13 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) PassTLSCert: passTLSCert, Routes: make(map[string]types.Route), Priority: priority, - BasicAuth: basicAuthCreds, WhiteList: getWhiteList(i), Redirect: getFrontendRedirect(i), EntryPoints: entryPoints, Headers: getHeader(i), Errors: getErrorPages(i), RateLimit: getRateLimit(i), + Auth: auth, } } @@ -438,67 +438,11 @@ func getRuleForHost(host string) string { return "Host:" + host } -func handleBasicAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) ([]string, error) { - annotationAuthType := getAnnotationName(i.Annotations, annotationKubernetesAuthType) - authType, exists := i.Annotations[annotationAuthType] - if !exists { - return nil, nil - } - - if strings.ToLower(authType) != "basic" { - return nil, fmt.Errorf("unsupported auth-type on annotation ingress.kubernetes.io/auth-type: %q", authType) - } - - authSecret := getStringValue(i.Annotations, annotationKubernetesAuthSecret, "") - if authSecret == "" { - return nil, errors.New("auth-secret annotation ingress.kubernetes.io/auth-secret must be set") - } - - basicAuthCreds, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient) - if err != nil { - return nil, fmt.Errorf("failed to load auth credentials: %s", err) - } - - return basicAuthCreds, nil -} - -func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) { - secret, ok, err := k8sClient.GetSecret(namespace, secretName) - switch { // keep order of case conditions - case err != nil: - return nil, fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err) - case !ok: - return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName) - case secret == nil: - return nil, fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName) - case len(secret.Data) != 1: - return nil, fmt.Errorf("found %d elements for secret %q/%q, must be single element exactly", len(secret.Data), namespace, secretName) - default: - } - var firstSecret []byte - for _, v := range secret.Data { - firstSecret = v - break - } - creds := make([]string, 0) - scanner := bufio.NewScanner(bytes.NewReader(firstSecret)) - for scanner.Scan() { - if cred := scanner.Text(); cred != "" { - creds = append(creds, cred) - } - } - if len(creds) == 0 { - return nil, fmt.Errorf("secret %q/%q does not contain any credentials", namespace, secretName) - } - - return creds, nil -} - func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Configuration, error) { var tlsConfigs []*tls.Configuration for _, t := range ingress.Spec.TLS { - tlsSecret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName) + secret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName) if err != nil { return nil, fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err) } @@ -506,19 +450,9 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config return nil, fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName) } - tlsCrtData, tlsCrtExists := tlsSecret.Data["tls.crt"] - tlsKeyData, tlsKeyExists := tlsSecret.Data["tls.key"] - - var missingEntries []string - if !tlsCrtExists { - missingEntries = append(missingEntries, "tls.crt") - } - if !tlsKeyExists { - missingEntries = append(missingEntries, "tls.key") - } - if len(missingEntries) > 0 { - return nil, fmt.Errorf("secret %s/%s is missing the following TLS data entries: %s", - ingress.Namespace, t.SecretName, strings.Join(missingEntries, ", ")) + cert, key, err := getCertificateBlocks(secret, ingress.Namespace, t.SecretName) + if err != nil { + return nil, err } entryPoints := getSliceStringValue(ingress.Annotations, annotationKubernetesFrontendEntryPoints) @@ -526,8 +460,8 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config tlsConfig := &tls.Configuration{ EntryPoints: entryPoints, Certificate: &tls.Certificate{ - CertFile: tls.FileOrContent(tlsCrtData), - KeyFile: tls.FileOrContent(tlsKeyData), + CertFile: tls.FileOrContent(cert), + KeyFile: tls.FileOrContent(key), }, } @@ -537,6 +471,42 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config return tlsConfigs, nil } +func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) (string, string, error) { + var missingEntries []string + + tlsCrtData, tlsCrtExists := secret.Data["tls.crt"] + if !tlsCrtExists { + missingEntries = append(missingEntries, "tls.crt") + } + + tlsKeyData, tlsKeyExists := secret.Data["tls.key"] + if !tlsKeyExists { + missingEntries = append(missingEntries, "tls.key") + } + + if len(missingEntries) > 0 { + return "", "", fmt.Errorf("secret %s/%s is missing the following TLS data entries: %s", + namespace, secretName, strings.Join(missingEntries, ", ")) + } + + cert := string(tlsCrtData[:]) + if cert == "" { + missingEntries = append(missingEntries, "tls.crt") + } + + key := string(tlsKeyData[:]) + if key == "" { + missingEntries = append(missingEntries, "tls.key") + } + + if len(missingEntries) > 0 { + return "", "", fmt.Errorf("secret %s/%s contains the following empty TLS data entries: %s", + namespace, secretName, strings.Join(missingEntries, ", ")) + } + + return cert, key, nil +} + // endpointPortNumber returns the port to be used for this endpoint. It is zero // if the endpoint does not match the given service port. func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int32 { @@ -573,6 +543,160 @@ func (p *Provider) shouldProcessIngress(annotationIngressClass string) bool { return annotationIngressClass == p.IngressClass } +func getAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Auth, error) { + authType := getStringValue(i.Annotations, annotationKubernetesAuthType, "") + if len(authType) == 0 { + return nil, nil + } + + auth := &types.Auth{ + HeaderField: getStringValue(i.Annotations, annotationKubernetesAuthHeaderField, ""), + } + + switch strings.ToLower(authType) { + case "basic": + basic, err := getBasicAuthConfig(i, k8sClient) + if err != nil { + return nil, err + } + + auth.Basic = basic + case "digest": + digest, err := getDigestAuthConfig(i, k8sClient) + if err != nil { + return nil, err + } + + auth.Digest = digest + case "forward": + forward, err := getForwardAuthConfig(i, k8sClient) + if err != nil { + return nil, err + } + + auth.Forward = forward + default: + return nil, fmt.Errorf("unsupported auth-type on annotation %s: %s", annotationKubernetesAuthType, authType) + } + + return auth, nil +} + +func getBasicAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Basic, error) { + credentials, err := getAuthCredentials(i, k8sClient) + if err != nil { + return nil, err + } + + return &types.Basic{Users: credentials}, nil +} + +func getDigestAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Digest, error) { + credentials, err := getAuthCredentials(i, k8sClient) + if err != nil { + return nil, err + } + + return &types.Digest{Users: credentials}, nil +} + +func getAuthCredentials(i *extensionsv1beta1.Ingress, k8sClient Client) ([]string, error) { + authSecret := getStringValue(i.Annotations, annotationKubernetesAuthSecret, "") + if authSecret == "" { + return nil, fmt.Errorf("auth-secret annotation %s must be set", annotationKubernetesAuthSecret) + } + + auth, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient) + if err != nil { + return nil, fmt.Errorf("failed to load auth credentials: %s", err) + } + + return auth, nil +} + +func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) { + secret, ok, err := k8sClient.GetSecret(namespace, secretName) + if err != nil { + return nil, fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err) + } + if !ok { + return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName) + } + if secret == nil { + return nil, fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName) + } + if len(secret.Data) != 1 { + return nil, fmt.Errorf("found %d elements for secret %q/%q, must be single element exactly", len(secret.Data), namespace, secretName) + } + + var firstSecret []byte + for _, v := range secret.Data { + firstSecret = v + break + } + + var credentials []string + scanner := bufio.NewScanner(bytes.NewReader(firstSecret)) + for scanner.Scan() { + if cred := scanner.Text(); len(cred) > 0 { + credentials = append(credentials, cred) + } + } + + if len(credentials) == 0 { + return nil, fmt.Errorf("secret %q/%q does not contain any credentials", namespace, secretName) + } + + return credentials, nil +} + +func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Forward, error) { + authURL := getStringValue(i.Annotations, annotationKubernetesAuthForwardURL, "") + if len(authURL) == 0 { + return nil, fmt.Errorf("forward authentication requires a url") + } + + forwardAuth := &types.Forward{ + Address: authURL, + TrustForwardHeader: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTrustHeaders, false), + AuthResponseHeaders: getSliceStringValue(i.Annotations, annotationKubernetesAuthForwardResponseHeaders), + } + + authSecretName := getStringValue(i.Annotations, annotationKubernetesAuthForwardTLSSecret, "") + if len(authSecretName) > 0 { + authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient) + if err != nil { + return nil, fmt.Errorf("failed to load auth secret: %s", err) + } + + forwardAuth.TLS = &types.ClientTLS{ + Cert: authSecretCert, + Key: authSecretKey, + InsecureSkipVerify: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTLSInsecure, false), + } + } + + return forwardAuth, nil +} + +func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) { + secret, exists, err := k8sClient.GetSecret(namespace, secretName) + if err != nil { + return "", "", fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err) + } + if !exists { + return "", "", fmt.Errorf("secret %q/%q does not exist", namespace, secretName) + } + if secret == nil { + return "", "", fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName) + } + if len(secret.Data) != 2 { + return "", "", fmt.Errorf("found %d elements for secret %q/%q, must be two elements exactly", len(secret.Data), namespace, secretName) + } + + return getCertificateBlocks(secret, namespace, secretName) +} + func getFrontendRedirect(i *extensionsv1beta1.Ingress) *types.Redirect { permanent := getBoolValue(i.Annotations, annotationKubernetesRedirectPermanent, false) diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index eacd36d26..6c9a752f2 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "reflect" "testing" "time" @@ -1032,7 +1031,7 @@ rateset: ), frontend("basic/auth", passHostHeader(), - basicAuth("myUser:myEncodedPW"), + basicAuthDeprecated("myUser:myEncodedPW"), routes( route("/auth", "PathPrefix:/auth"), route("basic", "Host:basic")), @@ -1680,7 +1679,7 @@ func TestMissingResources(t *testing.T) { assert.Equal(t, expected, actual) } -func TestBasicAuthInTemplate(t *testing.T) { +func TestLoadIngressesBasicAuth(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), @@ -1734,9 +1733,372 @@ func TestBasicAuthInTemplate(t *testing.T) { actual = provider.loadConfig(*actual) require.NotNil(t, actual) - got := actual.Frontends["basic/auth"].BasicAuth - if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) { - t.Fatalf("unexpected credentials: %+v", got) + got := actual.Frontends["basic/auth"].Auth.Basic.Users + assert.Equal(t, types.Users{"myUser:myEncodedPW"}, got) +} + +func TestLoadIngressesForwardAuth(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAuthType, "forward"), + iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), + iAnnotation(annotationKubernetesAuthForwardTrustHeaders, "true"), + iAnnotation(annotationKubernetesAuthForwardResponseHeaders, "X-Auth,X-Test,X-Secret"), + iRules( + iRule(iHost("foo"), + iPaths( + onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, ""))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + watchChan: watchChan, + } + provider := Provider{} + + actual, err := provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + expected := buildConfiguration( + backends( + backend("foo/bar", + lbMethod("wrr"), + servers( + server("http://10.10.0.1:8080", weight(1))), + ), + ), + frontends( + frontend("foo/bar", + passHostHeader(), + auth(forwardAuth("https://auth.host", + fwdTrustForwardHeader(), + fwdAuthResponseHeaders("X-Auth", "X-Test", "X-Secret"))), + routes( + route("/bar", "PathPrefix:/bar"), + route("foo", "Host:foo")), + ), + ), + ) + + assert.Equal(t, expected, actual) +} + +func TestLoadIngressesForwardAuthMissingURL(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAuthType, "forward"), + iRules( + iRule(iHost("foo"), + iPaths( + onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, ""))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + watchChan: watchChan, + } + provider := Provider{} + + actual, err := provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + expected := buildConfiguration( + backends( + backend("foo/bar", + lbMethod("wrr"), + servers(), + ), + ), + frontends(), + ) + + assert.Equal(t, expected, actual) +} + +func TestLoadIngressesForwardAuthWithTLSSecret(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAuthType, "forward"), + iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), + iAnnotation(annotationKubernetesAuthForwardTLSSecret, "secret"), + iAnnotation(annotationKubernetesAuthForwardTLSInsecure, "true"), + iRules( + iRule(iHost("foo"), + iPaths( + onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + secrets := []*corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + UID: "1", + Namespace: "testing", + }, + Data: map[string][]byte{ + "tls.crt": []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), + "tls.key": []byte("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), + }, + }} + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, ""))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + secrets: secrets, + watchChan: watchChan, + } + provider := Provider{} + + actual, err := provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + expected := buildConfiguration( + backends( + backend("foo/bar", + lbMethod("wrr"), + servers( + server("http://10.10.0.1:8080", weight(1))), + ), + ), + frontends( + frontend("foo/bar", + passHostHeader(), + auth( + forwardAuth("https://auth.host", + fwdAuthTLS( + "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", + "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", + true))), + routes( + route("/bar", "PathPrefix:/bar"), + route("foo", "Host:foo")), + ), + ), + ) + + assert.Equal(t, expected, actual) +} + +func TestLoadIngressesForwardAuthWithTLSSecretFailures(t *testing.T) { + tests := []struct { + desc string + secretName string + certName string + certData string + keyName string + keyData string + }{ + { + desc: "empty certificate and key", + secretName: "secret", + certName: "", + certData: "", + keyName: "", + keyData: "", + }, + { + desc: "wrong secret name, empty certificate and key", + secretName: "wrongSecret", + certName: "", + certData: "", + keyName: "", + keyData: "", + }, + { + desc: "empty certificate data", + secretName: "secret", + certName: "tls.crt", + certData: "", + keyName: "tls.key", + keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", + }, + { + desc: "empty key data", + secretName: "secret", + certName: "tls.crt", + certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", + keyName: "tls.key", + keyData: "", + }, + { + desc: "wrong cert name", + secretName: "secret", + certName: "cert.crt", + certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----", + keyName: "tls.key", + keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", + }, + { + desc: "wrong key name", + secretName: "secret", + certName: "tls.crt", + certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", + keyName: "cert.key", + keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", + }, + } + + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAuthType, "forward"), + iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), + iAnnotation(annotationKubernetesAuthForwardTLSSecret, "secret"), + iRules( + iRule(iHost("foo"), + iPaths( + onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, ""))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + secrets := []*corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: test.secretName, + UID: "1", + Namespace: "testing", + }, + Data: map[string][]byte{ + test.certName: []byte(test.certData), + test.keyName: []byte(test.keyData), + }, + }} + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + secrets: secrets, + watchChan: watchChan, + } + provider := Provider{} + + actual, err := provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + expected := buildConfiguration( + backends( + backend("foo/bar", + lbMethod("wrr"), + servers(), + ), + ), + frontends(), + ) + + assert.Equal(t, expected, actual) + }) + } } diff --git a/server/server_middlewares.go b/server/server_middlewares.go index 58f4c9034..d3abdfb96 100644 --- a/server/server_middlewares.go +++ b/server/server_middlewares.go @@ -108,6 +108,17 @@ func (s *Server) buildMiddlewares(frontendName string, frontend *types.Frontend, middle = append(middle, handler) } + // Authentication + if frontend.Auth != nil { + authMiddleware, err := mauth.NewAuthenticator(frontend.Auth, s.tracingMiddleware) + if err != nil { + return nil, nil, nil, err + } + + handler := s.wrapNegroniHandlerWithAccessLog(authMiddleware, fmt.Sprintf("Auth for %s", frontendName)) + middle = append(middle, handler) + } + return middle, buildModifyResponse(secureMiddleware, headerMiddleware), postConfig, nil } diff --git a/server/server_middlewares_test.go b/server/server_middlewares_test.go index 8b9651268..06b6a3311 100644 --- a/server/server_middlewares_test.go +++ b/server/server_middlewares_test.go @@ -251,3 +251,40 @@ func TestBuildRedirectHandler(t *testing.T) { }) } } + +func TestServerGenericFrontendAuthFail(t *testing.T) { + globalConfig := configuration.GlobalConfiguration{ + EntryPoints: configuration.EntryPoints{ + "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, + }, + } + + dynamicConfigs := types.Configurations{ + "config": &types.Configuration{ + Frontends: map[string]*types.Frontend{ + "frontend": { + EntryPoints: []string{"http"}, + Backend: "backend", + BasicAuth: []string{""}, + }, + }, + Backends: map[string]*types.Backend{ + "backend": { + Servers: map[string]types.Server{ + "server": { + URL: "http://localhost", + }, + }, + LoadBalancer: &types.LoadBalancer{ + Method: "Wrr", + }, + }, + }, + }, + } + + srv := NewServer(globalConfig, nil, nil) + + _, err := srv.loadConfig(dynamicConfigs, globalConfig) + require.NoError(t, err) +} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 31a19e1d2..08cdc41c6 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -56,6 +56,41 @@ "{{.}}", {{end}}] + {{if $frontend.Auth }} + [frontends."{{ $frontendName }}".auth] + headerField = "X-WebAuth-User" + + {{if $frontend.Auth.Basic }} + [frontends."{{ $frontendName }}".auth.basic] + users = [{{range $frontend.Auth.Basic.Users }} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Auth.Digest }} + [frontends."{{ $frontendName }}".auth.digest] + users = [{{range $frontend.Auth.Digest.Users }} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Auth.Forward }} + [frontends."{{ $frontendName }}".auth.forward] + address = "{{ $frontend.Auth.Forward.Address }}" + authResponseHeaders = [{{range $frontend.Auth.Forward.AuthResponseHeaders }} + "{{.}}", + {{end}}] + trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} + {{if $frontend.Auth.Forward.TLS }} + [frontends."{{ $frontendName }}".auth.forward.tls] + cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" + key = "{{ $frontend.Auth.Forward.TLS.Key }}" + insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} + {{end}} + {{end}} + + {{end}} + {{if $frontend.WhiteList }} [frontends."{{ $frontendName }}".whiteList] sourceRange = [{{range $frontend.WhiteList.SourceRange }} diff --git a/testhelpers/config.go b/testhelpers/config.go index 71969cc7e..b4fe53e80 100644 --- a/testhelpers/config.go +++ b/testhelpers/config.go @@ -138,12 +138,20 @@ func WithRoute(name string, rule string) func(*types.Route) string { } // WithBasicAuth is a helper to create a configuration +// Deprecated func WithBasicAuth(username string, password string) func(*types.Frontend) { return func(fe *types.Frontend) { fe.BasicAuth = []string{username + ":" + password} } } +// WithFrontEndAuth is a helper to create a configuration +func WithFrontEndAuth(auth *types.Auth) func(*types.Frontend) { + return func(fe *types.Frontend) { + fe.Auth = auth + } +} + // WithLBSticky is a helper to create a configuration func WithLBSticky(cookieName string) func(*types.Backend) { return func(b *types.Backend) { diff --git a/types/types.go b/types/types.go index 344e5aba2..9c7e076d7 100644 --- a/types/types.go +++ b/types/types.go @@ -184,13 +184,14 @@ type Frontend struct { PassHostHeader bool `json:"passHostHeader,omitempty"` PassTLSCert bool `json:"passTLSCert,omitempty"` Priority int `json:"priority"` - BasicAuth []string `json:"basicAuth"` + BasicAuth []string `json:"basicAuth"` // Deprecated WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` // Deprecated WhiteList *WhiteList `json:"whiteList,omitempty"` Headers *Headers `json:"headers,omitempty"` Errors map[string]*ErrorPage `json:"errors,omitempty"` RateLimit *RateLimit `json:"ratelimit,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` + Auth *Auth `json:"auth,omitempty"` } // Hash returns the hash value of a Frontend struct.