diff --git a/docs/toml.md b/docs/toml.md index dc9c98454..7a8cc95d1 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -1186,6 +1186,22 @@ Additionally, an annotation can be used on Kubernetes services to set the [circu - `traefik.backend.circuitbreaker: `: 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: @@ -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 diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index a1a660210..7fa806a59 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -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 @@ -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) diff --git a/provider/kubernetes/client.go b/provider/kubernetes/client.go index e6aec1ed6..d6670a278 100644 --- a/provider/kubernetes/client.go +++ b/provider/kubernetes/client.go @@ -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) } @@ -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 } @@ -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( @@ -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) { @@ -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) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index b52adf607..947274e7c 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -1,6 +1,9 @@ package kubernetes import ( + "bufio" + "bytes" + "errors" "fmt" "os" "reflect" @@ -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" ) @@ -29,6 +33,8 @@ const ( ruleTypePathPrefix = "PathPrefix" ) +const traefikDefaultRealm = "traefik" + // Provider holds configurations of the provider. type Provider struct { provider.BaseProvider `mapstructure:",squash"` @@ -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 { @@ -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 diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index e3c4a598f..5cb0219ae 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1466,6 +1466,35 @@ func TestIngressAnnotations(t *testing.T) { }, }, }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "ingress.kubernetes.io/auth-type": "basic", + "ingress.kubernetes.io/auth-secret": "mySecret", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "basic", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/auth", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", @@ -1515,12 +1544,25 @@ func TestIngressAnnotations(t *testing.T) { }, }, } + secrets := []*v1.Secret{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "mySecret", + UID: "1", + Namespace: "testing", + }, + Data: map[string][]byte{ + "auth": []byte("myUser:myEncodedPW"), + }, + }, + } endpoints := []*v1.Endpoints{} watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, + secrets: secrets, endpoints: endpoints, watchChan: watchChan, } @@ -1558,6 +1600,19 @@ func TestIngressAnnotations(t *testing.T) { Method: "wrr", }, }, + "basic/auth": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Sticky: false, + Method: "wrr", + }, + }, }, Frontends: map[string]*types.Frontend{ "foo/bar": { @@ -1586,6 +1641,20 @@ func TestIngressAnnotations(t *testing.T) { }, }, }, + "basic/auth": { + Backend: "basic/auth", + PassHostHeader: true, + Priority: len("/auth"), + Routes: map[string]types.Route{ + "/auth": { + Rule: "PathPrefix:/auth", + }, + "basic": { + Rule: "Host:basic", + }, + }, + BasicAuth: []string{"myUser:myEncodedPW"}, + }, }, } @@ -2030,13 +2099,102 @@ func TestMissingResources(t *testing.T) { } } +func TestBasicAuthInTemplate(t *testing.T) { + ingresses := []*v1beta1.Ingress{ + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "ingress.kubernetes.io/auth-type": "basic", + "ingress.kubernetes.io/auth-secret": "mySecret", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "basic", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/auth", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + services := []*v1.Service{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "service1", + UID: "1", + Namespace: "testing", + }, + Spec: v1.ServiceSpec{ + ClusterIP: "10.0.0.1", + Type: "ExternalName", + ExternalName: "example.com", + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + }, + } + secrets := []*v1.Secret{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "mySecret", + UID: "1", + Namespace: "testing", + }, + Data: map[string][]byte{ + "auth": []byte("myUser:myEncodedPW"), + }, + }, + } + + endpoints := []*v1.Endpoints{} + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + secrets: secrets, + endpoints: endpoints, + watchChan: watchChan, + } + provider := Provider{} + actual, err := provider.loadIngresses(client) + if err != nil { + t.Fatalf("error %+v", err) + } + + actual = provider.loadConfig(*actual) + got := actual.Frontends["basic/auth"].BasicAuth + if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) { + t.Fatalf("unexpected credentials: %+v", got) + } +} + type clientMock struct { ingresses []*v1beta1.Ingress services []*v1.Service + secrets []*v1.Secret endpoints []*v1.Endpoints watchChan chan interface{} apiServiceError error + apiSecretError error apiEndpointsError error } @@ -2064,6 +2222,19 @@ func (c clientMock) GetService(namespace, name string) (*v1.Service, bool, error return nil, false, nil } +func (c clientMock) GetSecret(namespace, name string) (*v1.Secret, bool, error) { + if c.apiSecretError != nil { + return nil, false, c.apiSecretError + } + + for _, secret := range c.secrets { + if secret.Namespace == namespace && secret.Name == name { + return secret, true, nil + } + } + return nil, false, nil +} + func (c clientMock) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) { if c.apiEndpointsError != nil { return nil, false, c.apiEndpointsError diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 1e4683786..d11a34b61 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -20,6 +20,9 @@ backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} + basicAuth = [{{range $frontend.BasicAuth}} + "{{.}}", + {{end}}] {{range $routeName, $route := $frontend.Routes}} [frontends."{{$frontendName}}".routes."{{$routeName}}"] rule = "{{$route.Rule}}"