Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic auth to kubernetes provider #1488

Merged
merged 2 commits into from
May 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,22 @@ Additionally, an annotation can be used on Kubernetes services to set the [circu

- `traefik.backend.circuitbreaker: <expression>`: set the circuit breaker expression for the backend (Default: nil).

### Authentication

Is possible to add additional authentication annotations in the Ingress rule.
The source of the authentication is a secret that contains usernames and passwords inside the the key auth.

- `ingress.kubernetes.io/auth-type`: `basic`
- `ingress.kubernetes.io/auth-secret`: contains the usernames and passwords with access to the paths defined in the Ingress Rule.

The secret must be created in the same namespace as the Ingress rule.

Limitations:

- Basic authentication only.
- Realm not configurable; only `traefik` default.
- Secret must contain only single file.

## Consul backend

Træfik can be configured to use Consul as a backend configuration:
Expand Down Expand Up @@ -1719,7 +1735,6 @@ RefreshSeconds = 15

Items in the `dynamodb` table must have three attributes:


- `id` : string
- The id is the primary key.
- `name` : string
Expand Down
24 changes: 13 additions & 11 deletions docs/user-guide/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,19 +505,22 @@ You should now be able to visit the websites in your browser.
* [cheeses.local/wensleydale](http://cheeses.local/wensleydale/)

## Disable passing the Host header
By default Træfik will pass the incoming Host header on to the upstream resource. There
are times however where you may not want this to be the case. For example if your service
is of the ExternalName type.

By default Træfik will pass the incoming Host header on to the upstream resource.
There are times however where you may not want this to be the case.
For example if your service is of the ExternalName type.

### Disable entirely

Add the following to your toml config:
```toml
disablePassHostHeaders = true
```

### Disable per ingress
To disable passing the Host header per ingress resource set the "traefik.frontend.passHostHeader"
annotation on your ingress to "false".

To disable passing the Host header per ingress resource set the `traefik.frontend.passHostHeader`
annotation on your ingress to `false`.

Here is an example ingress definition:
```yaml
Expand Down Expand Up @@ -557,16 +560,15 @@ If you were to visit example.com/static the request would then be passed onto
static.otherdomain.com/static and static.otherdomain.com would receive the
request with the Host header being static.otherdomain.com.

Note: The per ingress annotation overides whatever the global value is set to. So you
could set `disablePassHostHeaders` to true in your toml file and then enable passing
Note: The per ingress annotation overides whatever the global value is set to.
So you could set `disablePassHostHeaders` to `true` in your toml file and then enable passing
the host header per ingress if you wanted.

## Excluding an ingress from Træfik

You can control which ingress Træfik cares about by using the `kubernetes.io/ingress.class`
annotation. By default if the annotation is not set at all Træfik will include the
ingress. If the annotation is set to anything other than traefik or a blank string
Træfik will ignore it.
You can control which ingress Træfik cares about by using the `kubernetes.io/ingress.class` annotation.
By default if the annotation is not set at all Træfik will include the ingress.
If the annotation is set to anything other than traefik or a blank string Træfik will ignore it.


![](http://i.giphy.com/ujUdrdpX7Ok5W.gif)
29 changes: 29 additions & 0 deletions provider/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const resyncPeriod = time.Minute * 5
type Client interface {
GetIngresses(namespaces Namespaces) []*v1beta1.Ingress
GetService(namespace, name string) (*v1.Service, bool, error)
GetSecret(namespace, name string) (*v1.Secret, bool, error)
GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error)
WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error)
}
Expand All @@ -34,10 +35,12 @@ type clientImpl struct {
ingController *cache.Controller
svcController *cache.Controller
epController *cache.Controller
secController *cache.Controller

ingStore cache.Store
svcStore cache.Store
epStore cache.Store
secStore cache.Store

clientset *kubernetes.Clientset
}
Expand Down Expand Up @@ -154,6 +157,16 @@ func (c *clientImpl) GetService(namespace, name string) (*v1.Service, bool, erro
return service, exists, err
}

func (c *clientImpl) GetSecret(namespace, name string) (*v1.Secret, bool, error) {
var secret *v1.Secret
item, exists, err := c.secStore.GetByKey(namespace + "/" + name)
if err == nil && item != nil {
secret = item.(*v1.Secret)
}

return secret, exists, err
}

// WatchServices starts the watch of Provider Service resources and updates the corresponding store
func (c *clientImpl) WatchServices(watchCh chan<- interface{}, stopCh <-chan struct{}) {
source := cache.NewListWatchFromClient(
Expand Down Expand Up @@ -199,6 +212,21 @@ func (c *clientImpl) WatchEndpoints(watchCh chan<- interface{}, stopCh <-chan st
go c.epController.Run(stopCh)
}

func (c *clientImpl) WatchSecrets(watchCh chan<- interface{}, stopCh <-chan struct{}) {
source := cache.NewListWatchFromClient(
c.clientset.CoreV1().RESTClient(),
"secrets",
api.NamespaceAll,
fields.Everything())

c.secStore, c.secController = cache.NewInformer(
source,
&v1.Endpoints{},
resyncPeriod,
newResourceEventHandlerFuncs(watchCh))
go c.secController.Run(stopCh)
}

// WatchAll returns events in the cluster and updates the stores via informer
// Filters ingresses by labelSelector
func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error) {
Expand All @@ -213,6 +241,7 @@ func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-c
c.WatchIngresses(kubeLabelSelector, eventCh, stopCh)
c.WatchServices(eventCh, stopCh)
c.WatchEndpoints(eventCh, stopCh)
c.WatchSecrets(eventCh, stopCh)

go func() {
defer close(watchCh)
Expand Down
64 changes: 64 additions & 0 deletions provider/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package kubernetes

import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"reflect"
Expand All @@ -16,6 +19,7 @@ import (
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"k8s.io/client-go/pkg/util/intstr"
)

Expand All @@ -29,6 +33,8 @@ const (
ruleTypePathPrefix = "PathPrefix"
)

const traefikDefaultRealm = "traefik"

// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash"`
Expand Down Expand Up @@ -159,13 +165,21 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
default:
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
}
if realm := i.Annotations["ingress.kubernetes.io/auth-realm"]; realm != "" && realm != traefikDefaultRealm {
return nil, errors.New("no realm customization supported")
}

if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil {
return nil, err
}
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path,
PassHostHeader: PassHostHeader,
Routes: make(map[string]types.Route),
Priority: len(pa.Path),
BasicAuth: basicAuthCreds,
}
}
if len(r.Host) > 0 {
Expand Down Expand Up @@ -278,6 +292,56 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
return &templateObjects, nil
}

func handleBasicAuthConfig(i *v1beta1.Ingress, k8sClient Client) ([]string, error) {
authType, exists := i.Annotations["ingress.kubernetes.io/auth-type"]
if !exists {
return nil, nil
}
if strings.ToLower(authType) != "basic" {
return nil, fmt.Errorf("unsupported auth-type: %q", authType)
}
authSecret := i.Annotations["ingress.kubernetes.io/auth-secret"]
if authSecret == "" {
return nil, errors.New("auth-secret annotation must be set")
}
basicAuthCreds, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient)
if err != nil {
return nil, err
}
if len(basicAuthCreds) == 0 {
return nil, errors.New("secret file without credentials")
}
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, errors.New("secret data must not be nil")
case len(secret.Data) != 1:
return nil, errors.New("secret must contain single element only")
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)
}
}
return creds, nil
}

func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
if len(endpointPorts) > 0 {
//name is optional if there is only one port
Expand Down