Auth support in frontends for k8s and file
This commit is contained in:
parent
e8e36bd9d5
commit
bb14ec70bd
14 changed files with 867 additions and 181 deletions
|
@ -1103,6 +1103,41 @@ var _templatesKubernetesTmpl = []byte(`[backends]
|
||||||
"{{.}}",
|
"{{.}}",
|
||||||
{{end}}]
|
{{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 }}
|
{{if $frontend.WhiteList }}
|
||||||
[frontends."{{ $frontendName }}".whiteList]
|
[frontends."{{ $frontendName }}".whiteList]
|
||||||
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
|
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
|
||||||
|
|
|
@ -55,11 +55,38 @@ Træfik can be configured with a file.
|
||||||
passHostHeader = true
|
passHostHeader = true
|
||||||
passTLSCert = true
|
passTLSCert = true
|
||||||
priority = 42
|
priority = 42
|
||||||
|
|
||||||
|
# Use frontends.frontend1.auth.basic below instead
|
||||||
basicAuth = [
|
basicAuth = [
|
||||||
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
|
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
|
||||||
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
|
"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]
|
[frontends.frontend1.whiteList]
|
||||||
sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
|
sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
|
||||||
useXForwardedFor = true
|
useXForwardedFor = true
|
||||||
|
|
|
@ -141,7 +141,7 @@ 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:
|
The following general annotations are applicable on the Ingress object:
|
||||||
|
|
||||||
| Annotation | Description |
|
| Annotation | Description |
|
||||||
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
|
||||||
| `traefik.ingress.kubernetes.io/buffering: <YML>` | (3) See [buffering](/configuration/commons/#buffering) section. |
|
| `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/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/frontend-entry-points: http,https` | Override the default frontend endpoints. |
|
||||||
|
@ -297,14 +297,20 @@ The following security annotations are applicable on the Ingress object:
|
||||||
Additional authentication annotations can be added to 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.
|
The source of the authentication is a Secret object that contains the credentials.
|
||||||
|
|
||||||
| Annotation | Description |
|
| Annotation | basic | digest | forward | Description |
|
||||||
|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
|----------------------------------------------------------------------|-------|--------|---------|-------------------------------------------------------------------------------------------------------------|
|
||||||
| `ingress.kubernetes.io/auth-type: basic` | Contains the authentication type. The only permitted type is `basic`. |
|
| `ingress.kubernetes.io/auth-type: basic` | x | x | x | Contains the authentication type: `basic`, `digest`, `forward`. |
|
||||||
| `ingress.kubernetes.io/auth-secret: mysecret` | Name of Secret containing the username and password with access to the paths defined in the Ingress object. |
|
| `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 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 realm is not configurable; the only supported (and default) value is `traefik`.
|
||||||
- The Secret must contain a single file only.
|
- The Secret must contain a single file only.
|
||||||
|
|
|
@ -54,14 +54,13 @@
|
||||||
[entryPoints.http.auth.forward]
|
[entryPoints.http.auth.forward]
|
||||||
address = "https://authserver.com/auth"
|
address = "https://authserver.com/auth"
|
||||||
trustForwardHeader = true
|
trustForwardHeader = true
|
||||||
|
authResponseHeaders = ["X-Auth-User"]
|
||||||
[entryPoints.http.auth.forward.tls]
|
[entryPoints.http.auth.forward.tls]
|
||||||
ca = [ "path/to/local.crt"]
|
ca = [ "path/to/local.crt"]
|
||||||
caOptional = true
|
caOptional = true
|
||||||
cert = "path/to/foo.cert"
|
cert = "path/to/foo.cert"
|
||||||
key = "path/to/foo.key"
|
key = "path/to/foo.key"
|
||||||
insecureSkipVerify = true
|
insecureSkipVerify = true
|
||||||
[entryPoints.http.auth.forward]
|
|
||||||
authResponseHeaders = ["X-Auth-User"]
|
|
||||||
|
|
||||||
[entryPoints.http.proxyProtocol]
|
[entryPoints.http.proxyProtocol]
|
||||||
insecure = true
|
insecure = true
|
||||||
|
@ -273,6 +272,18 @@ Users can be specified directly in the TOML file, or indirectly by referencing a
|
||||||
usersFile = "/path/to/.htpasswd"
|
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
|
### Digest Authentication
|
||||||
|
|
||||||
You can use `htdigest` to generate them.
|
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"
|
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
|
### Forward Authentication
|
||||||
|
|
||||||
This configuration will first forward the request to `http://authserver.com/auth`.
|
This configuration will first forward the request to `http://authserver.com/auth`.
|
||||||
|
@ -313,8 +336,10 @@ Otherwise, the response from the authentication server is returned.
|
||||||
#
|
#
|
||||||
trustForwardHeader = true
|
trustForwardHeader = true
|
||||||
|
|
||||||
# Copy headers from the authentication server to the request
|
# Copy headers from the authentication server to the request.
|
||||||
[entryPoints.http.auth.forward]
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
authResponseHeaders = ["X-Auth-User", "X-Secret"]
|
authResponseHeaders = ["X-Auth-User", "X-Secret"]
|
||||||
|
|
||||||
# Enable forward auth TLS connection.
|
# Enable forward auth TLS connection.
|
||||||
|
@ -322,8 +347,10 @@ Otherwise, the response from the authentication server is returned.
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
[entryPoints.http.auth.forward.tls]
|
[entryPoints.http.auth.forward.tls]
|
||||||
cert = "authserver.crt"
|
ca = [ "path/to/local.crt"]
|
||||||
key = "authserver.key"
|
caOptional = true
|
||||||
|
cert = "path/to/foo.cert"
|
||||||
|
key = "path/to/foo.key"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Specify Minimum TLS Version
|
## Specify Minimum TLS Version
|
||||||
|
|
|
@ -322,42 +322,6 @@ The `consul` provider contains the configuration.
|
||||||
rule = "Path:/test"
|
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
|
## Override the Traefik HTTP server idleTimeout and/or throttle configurations from re-loading too quickly
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|
|
@ -9,6 +9,12 @@ const (
|
||||||
annotationKubernetesAuthRealm = "ingress.kubernetes.io/auth-realm"
|
annotationKubernetesAuthRealm = "ingress.kubernetes.io/auth-realm"
|
||||||
annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type"
|
annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type"
|
||||||
annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret"
|
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"
|
annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target"
|
||||||
annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range"
|
annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range"
|
||||||
annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for"
|
annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for"
|
||||||
|
|
|
@ -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) {
|
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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -221,9 +221,9 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := templateObjects.Frontends[baseName]; !exists {
|
if _, exists := templateObjects.Frontends[baseName]; !exists {
|
||||||
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
|
auth, err := getAuthConfig(i, k8sClient)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,13 +238,13 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
PassTLSCert: passTLSCert,
|
PassTLSCert: passTLSCert,
|
||||||
Routes: make(map[string]types.Route),
|
Routes: make(map[string]types.Route),
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
BasicAuth: basicAuthCreds,
|
|
||||||
WhiteList: getWhiteList(i),
|
WhiteList: getWhiteList(i),
|
||||||
Redirect: getFrontendRedirect(i),
|
Redirect: getFrontendRedirect(i),
|
||||||
EntryPoints: entryPoints,
|
EntryPoints: entryPoints,
|
||||||
Headers: getHeader(i),
|
Headers: getHeader(i),
|
||||||
Errors: getErrorPages(i),
|
Errors: getErrorPages(i),
|
||||||
RateLimit: getRateLimit(i),
|
RateLimit: getRateLimit(i),
|
||||||
|
Auth: auth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,67 +438,11 @@ func getRuleForHost(host string) string {
|
||||||
return "Host:" + host
|
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) {
|
func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Configuration, error) {
|
||||||
var tlsConfigs []*tls.Configuration
|
var tlsConfigs []*tls.Configuration
|
||||||
|
|
||||||
for _, t := range ingress.Spec.TLS {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err)
|
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)
|
return nil, fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsCrtData, tlsCrtExists := tlsSecret.Data["tls.crt"]
|
cert, key, err := getCertificateBlocks(secret, ingress.Namespace, t.SecretName)
|
||||||
tlsKeyData, tlsKeyExists := tlsSecret.Data["tls.key"]
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
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, ", "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entryPoints := getSliceStringValue(ingress.Annotations, annotationKubernetesFrontendEntryPoints)
|
entryPoints := getSliceStringValue(ingress.Annotations, annotationKubernetesFrontendEntryPoints)
|
||||||
|
@ -526,8 +460,8 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config
|
||||||
tlsConfig := &tls.Configuration{
|
tlsConfig := &tls.Configuration{
|
||||||
EntryPoints: entryPoints,
|
EntryPoints: entryPoints,
|
||||||
Certificate: &tls.Certificate{
|
Certificate: &tls.Certificate{
|
||||||
CertFile: tls.FileOrContent(tlsCrtData),
|
CertFile: tls.FileOrContent(cert),
|
||||||
KeyFile: tls.FileOrContent(tlsKeyData),
|
KeyFile: tls.FileOrContent(key),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -537,6 +471,42 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config
|
||||||
return tlsConfigs, nil
|
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
|
// endpointPortNumber returns the port to be used for this endpoint. It is zero
|
||||||
// if the endpoint does not match the given service port.
|
// if the endpoint does not match the given service port.
|
||||||
func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int32 {
|
func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int32 {
|
||||||
|
@ -573,6 +543,160 @@ func (p *Provider) shouldProcessIngress(annotationIngressClass string) bool {
|
||||||
return annotationIngressClass == p.IngressClass
|
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 {
|
func getFrontendRedirect(i *extensionsv1beta1.Ingress) *types.Redirect {
|
||||||
permanent := getBoolValue(i.Annotations, annotationKubernetesRedirectPermanent, false)
|
permanent := getBoolValue(i.Annotations, annotationKubernetesRedirectPermanent, false)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -1032,7 +1031,7 @@ rateset:
|
||||||
),
|
),
|
||||||
frontend("basic/auth",
|
frontend("basic/auth",
|
||||||
passHostHeader(),
|
passHostHeader(),
|
||||||
basicAuth("myUser:myEncodedPW"),
|
basicAuthDeprecated("myUser:myEncodedPW"),
|
||||||
routes(
|
routes(
|
||||||
route("/auth", "PathPrefix:/auth"),
|
route("/auth", "PathPrefix:/auth"),
|
||||||
route("basic", "Host:basic")),
|
route("basic", "Host:basic")),
|
||||||
|
@ -1680,7 +1679,7 @@ func TestMissingResources(t *testing.T) {
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuthInTemplate(t *testing.T) {
|
func TestLoadIngressesBasicAuth(t *testing.T) {
|
||||||
ingresses := []*extensionsv1beta1.Ingress{
|
ingresses := []*extensionsv1beta1.Ingress{
|
||||||
buildIngress(
|
buildIngress(
|
||||||
iNamespace("testing"),
|
iNamespace("testing"),
|
||||||
|
@ -1734,9 +1733,372 @@ func TestBasicAuthInTemplate(t *testing.T) {
|
||||||
|
|
||||||
actual = provider.loadConfig(*actual)
|
actual = provider.loadConfig(*actual)
|
||||||
require.NotNil(t, actual)
|
require.NotNil(t, actual)
|
||||||
got := actual.Frontends["basic/auth"].BasicAuth
|
got := actual.Frontends["basic/auth"].Auth.Basic.Users
|
||||||
if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) {
|
assert.Equal(t, types.Users{"myUser:myEncodedPW"}, got)
|
||||||
t.Fatalf("unexpected credentials: %+v", 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)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,17 @@ func (s *Server) buildMiddlewares(frontendName string, frontend *types.Frontend,
|
||||||
middle = append(middle, handler)
|
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
|
return middle, buildModifyResponse(secureMiddleware, headerMiddleware), postConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -56,6 +56,41 @@
|
||||||
"{{.}}",
|
"{{.}}",
|
||||||
{{end}}]
|
{{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 }}
|
{{if $frontend.WhiteList }}
|
||||||
[frontends."{{ $frontendName }}".whiteList]
|
[frontends."{{ $frontendName }}".whiteList]
|
||||||
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
|
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
|
||||||
|
|
|
@ -138,12 +138,20 @@ func WithRoute(name string, rule string) func(*types.Route) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBasicAuth is a helper to create a configuration
|
// WithBasicAuth is a helper to create a configuration
|
||||||
|
// Deprecated
|
||||||
func WithBasicAuth(username string, password string) func(*types.Frontend) {
|
func WithBasicAuth(username string, password string) func(*types.Frontend) {
|
||||||
return func(fe *types.Frontend) {
|
return func(fe *types.Frontend) {
|
||||||
fe.BasicAuth = []string{username + ":" + password}
|
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
|
// WithLBSticky is a helper to create a configuration
|
||||||
func WithLBSticky(cookieName string) func(*types.Backend) {
|
func WithLBSticky(cookieName string) func(*types.Backend) {
|
||||||
return func(b *types.Backend) {
|
return func(b *types.Backend) {
|
||||||
|
|
|
@ -184,13 +184,14 @@ type Frontend struct {
|
||||||
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
||||||
PassTLSCert bool `json:"passTLSCert,omitempty"`
|
PassTLSCert bool `json:"passTLSCert,omitempty"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
BasicAuth []string `json:"basicAuth"`
|
BasicAuth []string `json:"basicAuth"` // Deprecated
|
||||||
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` // Deprecated
|
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` // Deprecated
|
||||||
WhiteList *WhiteList `json:"whiteList,omitempty"`
|
WhiteList *WhiteList `json:"whiteList,omitempty"`
|
||||||
Headers *Headers `json:"headers,omitempty"`
|
Headers *Headers `json:"headers,omitempty"`
|
||||||
Errors map[string]*ErrorPage `json:"errors,omitempty"`
|
Errors map[string]*ErrorPage `json:"errors,omitempty"`
|
||||||
RateLimit *RateLimit `json:"ratelimit,omitempty"`
|
RateLimit *RateLimit `json:"ratelimit,omitempty"`
|
||||||
Redirect *Redirect `json:"redirect,omitempty"`
|
Redirect *Redirect `json:"redirect,omitempty"`
|
||||||
|
Auth *Auth `json:"auth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash returns the hash value of a Frontend struct.
|
// Hash returns the hash value of a Frontend struct.
|
||||||
|
|
Loading…
Reference in a new issue