Merge pull request #1488 from alpe/k8s-auth
Add basic auth to kubernetes provider
This commit is contained in:
commit
b5283391dd
6 changed files with 296 additions and 12 deletions
17
docs/toml.md
17
docs/toml.md
|
@ -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:
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}"
|
||||
|
|
Loading…
Reference in a new issue