Auth support in frontends for k8s and file

This commit is contained in:
Mikael Rapp 2018-07-02 11:52:04 +02:00 committed by Traefiker Bot
parent e8e36bd9d5
commit bb14ec70bd
14 changed files with 867 additions and 181 deletions

View file

@ -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 }}

View file

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

View file

@ -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: <YML>` | (3) See [buffering](/configuration/commons/#buffering) section. |
| `traefik.ingress.kubernetes.io/error-pages: <YML>` | (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: <YML>` | (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: <YML>` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) |
| Annotation | Description |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `traefik.ingress.kubernetes.io/buffering: <YML>` | (3) See [buffering](/configuration/commons/#buffering) section. |
| `traefik.ingress.kubernetes.io/error-pages: <YML>` | (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: <YML>` | (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: <YML>` | 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.

View file

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

View file

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

View file

@ -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"

View file

@ -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}
}
}

View file

@ -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)

View file

@ -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)
})
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 }}

View file

@ -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) {

View file

@ -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.