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

Add basic auth to kubernetes provider #1488

merged 2 commits into from
May 3, 2017

Conversation

alpe
Copy link
Contributor

@alpe alpe commented Apr 24, 2017

This will add the kubernetes provider for basic authentication. Thanks to @SantoDE for #1147.

Some constraints:

  • Basic authentication only
  • Realm not configurable; only traefikdefault
  • Secret is in same namespace as ingress rule
  • Secret must contain only single file (*)

(*) otherwise we can have some conventions here like key name (pattern)

Example

Based on https://docs.traefik.io/user-guide/kubernetes/

  • Encode credentials
    htpasswd -c ./auth jane
  • Deploy a secret to k8s
    kubectl --namespace=kube-system create secret generic mysecret --from-file auth
  • Deploy ingress
apiVersion: v1
kind: Service
metadata:
  name: traefik-web-ui
  namespace: kube-system
spec:
  selector:
    k8s-app: traefik-ingress-lb
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: traefik-web-ui
  namespace: kube-system
  annotations:
    ingress.kubernetes.io/auth-type: "basic"
    ingress.kubernetes.io/auth-secret: "mysecret"
spec:
  rules:
  - host: traefik-ui.local
    http:
      paths:
      - backend:
          serviceName: traefik-web-ui
          servicePort: 80

Note the two annotations to enable basic auth and link the secret

@ldez ldez added kind/enhancement a new or improved feature. area/provider/k8s/ingress labels Apr 24, 2017
Copy link
Contributor

@errm errm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM apart from the one small spelling error

return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName)
}
if secret == nil {
return nil, errors.New("secret data must nost be nil")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-nost- not

Copy link
Collaborator

@SantoDE SantoDE left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👼

@alpe squash and rebase please ;-)

Copy link
Contributor

@timoreimann timoreimann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments.

I notice that most of the unhappy paths are not test-covered. Can we improve here? You already added the necessary secret stub.

return nil, err
}
if len(basicAuthCreds) == 0 {
return nil, errors.New("secet file without credentials")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo (secet -> secret)

}
} else if authType != "" {
return nil, fmt.Errorf("unsupported auth-type: %q", authType)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please put the entire block in a separate function?

loadIngresses is already pretty big.

if len(basicAuthCreds) == 0 {
return nil, errors.New("secet file without credentials")
}
} else if authType != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this check before the happy path so that we don't need the else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition was authType != "" && strings.ToLower(authType) == "basic" for the error. The else block made sense but it's better in the extracted method.

}
if len(secret.Data) != 1 {
return nil, errors.New("secret must contain single element only")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block lends itself to a switch statement IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of the conditions matter. The switch works top down when multiple conditions are true. So that's fine but it feels more fragile that I'll add note to keep the order.

return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName)
}
if secret == nil {
return nil, errors.New("secret data must not be nil")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rephrase this without the nil for the user? What does a nil secret imply?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can this be nil anyway when ok is true? I was wondering a lot when I ran into this but I did not spent time investigating.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is it won't ever be nil unless ok is false or err is non-nil. The pointer is probably for performance / in-place-update reasons.

return nil, errors.New("secret data must not be nil")
}
if len(secret.Data) != 1 {
return nil, errors.New("secret must contain single element only")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improvement suggestion: "multiple secrets are not supported".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data can have 0 elements, too. I think this is a temporary solution until we have some convention for a key name or pattern.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

for _, v := range secret.Data {
firstElement = v
break
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not firstElement := secret.Data[0]? We have already asserted there's only a single element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data is a map

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, missed that. 👍

if len(secret.Data) != 1 {
return nil, errors.New("secret must contain single element only")
}
var firstElement []byte
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name is not ideal. Maybe firstSecret instead?

actual = provider.loadConfig(*actual)
got := actual.Frontends["basic/auth"].BasicAuth
if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) {
t.Fatalf("unexpected creadentials: %+v", got)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo.

@@ -2121,6 +2278,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.apiServiceError != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not reuse apiServiceError but get its own field.

@alpe
Copy link
Contributor Author

alpe commented Apr 24, 2017

I have applied most of the review comments @timoreimann requested. Thanks for the feedback! The test coverage and documentation can be better. I may be able to do a little bit more end of the week.

@timoreimann
Copy link
Contributor

Thanks @alpe, looking pretty good now.

A bit of documentation (especially with regards to the limitations mentioned in the PR) and coverage of the important unhappy paths would be great to have. 👍

@emilevauge
Copy link
Member

Ping @alpe

@ldez
Copy link
Contributor

ldez commented May 2, 2017

Documentation added

@kekoav
Copy link
Contributor

kekoav commented Jun 2, 2017

@alpe @timoreimann I know this is done but I was reviewing due to #1596 and saw this:
https://github.com/containous/traefik/blob/master/provider/kubernetes/client.go#L266

// fireEvent checks if all controllers have synced before firing
// Used after startup or a reconnect
func (c *clientImpl) fireEvent(event interface{}, eventCh chan interface{}) {
	if !c.ingController.HasSynced() || !c.svcController.HasSynced() || !c.epController.HasSynced() {
		return
	}
	eventHandlerFunc(eventCh, event)
}

Was excluding the new secController from this check for "all controllers" intentional?

@timoreimann
Copy link
Contributor

Side question: What do we need the secController for exactly?

I'm really not too deep into that part of the code. What I know though is that we use various events as a trigger to poll the k8s APIs. Does the secController make sure we do that for Secrets events as well? If so, can such events ever indicate a state change relevant for Traefik?

ldez pushed a commit to spinto/traefik that referenced this pull request Jul 7, 2017
The traefik controller shall have access to secrets for the k8s basic authentication (traefik#1488) to work
ldez pushed a commit that referenced this pull request Jul 7, 2017
The traefik controller shall have access to secrets for the k8s basic authentication (#1488) to work
@shadycuz
Copy link
Contributor

shadycuz commented Nov 7, 2017

@alpe These instuctions

Encode credentials
htpasswd -c ./auth jane
Deploy a secret to k8s
kubectl --namespace=kube-system create secret generic mysecret --from-file auth

are not on the traefik website and it was very hard to eventually track down this PR and find the example for creating the secret.

@alpe
Copy link
Contributor Author

alpe commented Nov 7, 2017

@shadycuz Apologies, this could have been better. I had very limited time to work on this when I contributed the code. I'm happy that you find it useful. I am very sure the community will appreciate it when you could help with some doc so that it is easier for others to setup the basic auth.

@shadycuz
Copy link
Contributor

shadycuz commented Nov 7, 2017

@alpe I can do that from what I know and what I see in this PR, but I have other questions, for example can we generate the secret according to instructions for generating the web dashboard auth?

Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones.

as long as its USERNAME:ENCRYPTEDPASSWORD ?

@ldez
Copy link
Contributor

ldez commented Nov 7, 2017

@erkolson
Copy link

erkolson commented Nov 10, 2017

Probably not the right place to post this, please tell me somewhere else that is more appropriate...

I think it is problematic to grant permission to get every secret in the kube-system namespace. It seems like the RBAC constraints should be limited to a specific secret resource or pattern for the secret name, for example:

rules:
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - get
    resourceNames:
      - "traefik-*"

A small constraint on the user's secret name would make it a lot safer to run in the kube-system namespace.

@ldez
Copy link
Contributor

ldez commented Nov 10, 2017

@erkolson Could ask your question on the Traefik community Slack channel or Stack Overflow using the "traefik" tag

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants