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 support for fetching k8s Ingress TLS data from secrets #2439
Conversation
@gopenguin Could you fix?
|
@ldez When I regenerate the file all file modes change from |
It's not the expected behavior. |
@ldez I've just pulled your changes and I think there is sill an issue with the generated file. It isn't go fmt'ed but the version in the repo is. |
If you just run go generate, the file changes but after running go fmt the changes are gone. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy with the code, looks clean, seems to have relevant unit tests. Thanks for working on this @gopenguin I think its a feature a lot of K8s folks have been waiting on.
I want to do a little manual testing before we merge, but I will wait for the generate stuff to be settled first.
We will need some documentation, but I am happy to help out/write it/whatever if you need @gopenguin?
I had some time this morning and wrote some docs but I realy appreciate any help. |
provider/kubernetes/kubernetes.go
Outdated
@@ -18,6 +18,7 @@ import ( | |||
"github.com/containous/traefik/log" | |||
"github.com/containous/traefik/provider" | |||
"github.com/containous/traefik/safe" | |||
traefikTls "github.com/containous/traefik/tls" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more conventional in Go to not camel case import names. I'd suggest ttls
or traefiktls
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought, I'd reuse the prefix used elswhere in this project like in acme/acme.go
, cmd/traefik/traefik.go
, server/server.go
and types/types.go
but I can't remember why I introduced the prefix in the first place so I will remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's quite possible we used camel case elsewhere. If we do, we should fix those (by a separate PR).
provider/kubernetes/kubernetes.go
Outdated
for _, t := range i.Spec.TLS { | ||
tlsSecret, exists, err := k8sClient.GetSecret(i.Namespace, t.SecretName) | ||
if err != nil { | ||
log.Errorf("Unable to retrive secret %s/%s: %s", i.Namespace, t.SecretName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: retrieve
provider/kubernetes/kubernetes.go
Outdated
continue | ||
} | ||
_, tlsCrtExists := tlsSecret.Data["tls.crt"] | ||
_, tlsKeyExists := tlsSecret.Data["tls.key"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We seem to need both ignored variables later, so let's store them here for reusal below.
provider/kubernetes/kubernetes.go
Outdated
} | ||
_, tlsCrtExists := tlsSecret.Data["tls.crt"] | ||
_, tlsKeyExists := tlsSecret.Data["tls.key"] | ||
if !tlsCrtExists || !tlsKeyExists { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we split this up into two dedicated errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there also be a specific error for both entries missing to speed up the errorhandling?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind
provider/kubernetes/kubernetes.go
Outdated
_, tlsCrtExists := tlsSecret.Data["tls.crt"] | ||
_, tlsKeyExists := tlsSecret.Data["tls.key"] | ||
if !tlsCrtExists || !tlsKeyExists { | ||
log.Warnf("Secret %s/%s is not of type 'kubernetes.io/tls'", i.Namespace, t.SecretName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the error message correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, no, this message isn't correct anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more minor thing.
Looks good overall. Will do another final check tonight.
@@ -2262,6 +2274,169 @@ func TestBasicAuthInTemplate(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestTlsSecret(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TLS should be all caps.
provider/kubernetes/kubernetes.go
Outdated
if len(t.Hosts) > 0 { | ||
hosts = t.Hosts | ||
} else { | ||
// If no hosts are provided the hosts property should default ot the wildcard host. To ensure that the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: ot
instead of to
.
provider/kubernetes/kubernetes.go
Outdated
for _, t := range i.Spec.TLS { | ||
tlsSecret, exists, err := k8sClient.GetSecret(i.Namespace, t.SecretName) | ||
if err != nil { | ||
log.Errorf("Unable to retrieve secret %s/%s: %s", i.Namespace, t.SecretName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use Failed instead of Unable to be consistent with the existing wording.
provider/kubernetes/kubernetes.go
Outdated
continue | ||
} | ||
if !exists { | ||
log.Warnf("Unable to find secret %s/%s", i.Namespace, t.SecretName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find here and retrieve above are very similar. Can we say something like Secret %s/%s does not exist?
provider/kubernetes/kubernetes.go
Outdated
} | ||
|
||
templateObjects.TLSConfiguration = append(templateObjects.TLSConfiguration, tlsConfig) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put the TLS logic into a dedicated function?
@@ -310,6 +311,7 @@ func TestLoadIngresses(t *testing.T) { | |||
}, | |||
}, | |||
}, | |||
TLSConfiguration: []*tls.Configuration{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it needed to specify an empty TLSConfiguration
for tests unrelated to the TLS functionality?
If no, then I'd prefer to omit those here and further below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I used the empty slice it was necessary (nil slice != empty slice) but I didn't know that you could append to a nil slice without any errors.
provider/kubernetes/kubernetes.go
Outdated
log.Warnf("Secret %s/%s doesn't have an entry named 'tls.key'", i.Namespace, t.SecretName) | ||
} | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the four error cases above seem to have test coverage. Can we improve here?
provider/kubernetes/kubernetes.go
Outdated
hosts = append(hosts, r.Host) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My code coverage indicator tells me that the else
branch does not have a corresponding test. We should change that.
provider/kubernetes/kubernetes.go
Outdated
// If no hosts are provided the hosts property should default ot the wildcard host. To ensure that the | ||
// tls config only matches rules in this ingress, add all the used hosts to simulate a wildcard like | ||
// behavior. | ||
hosts = []string{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If tls.Configuration.EntryPoints
is fine with nil slices, you can drop this line as we have already defined the nil slice in line 337, and Go can append to such slices without problems.
If tls.Configuration.EntryPoints
requires a non-nil empty slice, then you can replace line 337 with this one (i.e., initialize an empty slice right away).
Thanks for the review. I'll probably get it fixed by tomorrow evening. |
Move the extraction of the tls cert definition into its own method to improve readability and testability. Add tests for error cases and wildcard behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many thanks @gopenguin for this awesome PR ! 👏 👏
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some more nit-picks; I'm generally fine with the PR, just need to make sure that we handle error cases appropriately. (See one of my comments pointing to a previous discussion.)
docs/user-guide/kubernetes.md
Outdated
serviceName: traefik-web-ui | ||
servicePort: 80 | ||
tls: | ||
- hosts: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I don't see us parsing the hosts list in our code explicitly. Am I missing something?
docs/user-guide/kubernetes.md
Outdated
|
||
```shell | ||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=traefik-ui.minikube" | ||
kubectl -n kube-system create secret tls traefik-ui-tls-cert --key=/tmp/tls.key --cert=/tmp/tls.crt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd drop the /tmp
directory part as it may not exist on every system. Saving the key and cert in the current working directory seems good enough for me.
docs/user-guide/kubernetes.md
Outdated
!!! note | ||
For this example to work you need a TLS entrypoint. You don't have to provide a TLS certificate at this point. For more details see [here](/configuration/entrypoints/). | ||
|
||
To setup a HTTPS protected ingress, you can leverage the TLS feature of the ingress resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/a HTTPS protected/an HTTPS-protected/
docs/user-guide/kubernetes.md
Outdated
The secret must have two entries named `tls.key`and `tls.crt`. See the [kubernetes documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls) for more details. | ||
|
||
!!! note | ||
The TLS certificates will be added to all entrypoints defined by the ingress annotation `traefik.frontend.entryPoints`. If no such annotation is provided, the TLS certificates will be added to all TLS enabled `defaultEntryPoints`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/to all TLS enabled/to all TLS-enabled/
@@ -92,6 +92,23 @@ func iBackend(name string, port intstr.IntOrString) func(*v1beta1.HTTPIngressPat | |||
} | |||
} | |||
|
|||
func iTLSs(opts ...func(*v1beta1.IngressTLS)) func(*v1beta1.Ingress) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we rename this to iTLSes
? Makes it slightly more readable (and grammatically correct) IMHO.
provider/kubernetes/kubernetes.go
Outdated
|
||
tlsConfigs, err := getTLSConfigurations(i, k8sClient) | ||
if err != nil { | ||
log.Errorf("Error configuring TLS for ingress %s/%s: %v", i.Namespace, i.Name, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't figure out, if an entry point is TLS-enabled at this point as the default configuration (passing nil) is handled further down the road and override entry points are only available as strings without any context. Therefore, I can't check if an entry point is TLS-enabled or not.
But I can't imagine a way to solve this problem properly as either the internals need to know about a TLS configuration error and abort in the case of an HTTPS-only entry point or the provider needs to know about the entry points (default and override).
I'm sorry for not pointing this out earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing the research.
If we can't figure out whether it's safe to accept an illegal definition or not, then my opinion is that we should err on the side of safety and skip it. Better be safe than sorry.
What do you (and @dtomcej) think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Staying on the safe side should be our first choice as this is security critical. I'd also add a note to the docs regarding this and also the deviation from the kubernetes spec (host names are ignored and therefore the wildcard behaviour cannot be implemented).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you refresh my mind again: what's the reason we cannot comply with the Kubernetes spec? Is there a potential path (possibly in the future) we could improve on this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a kubernetes ingress resource multiple TLSes per ingress are allowed. Each consists of two parts: a list of host names and a reference to a kubernetes secret. There are two cases in which a TLS configuration can be:
- A list of host names is provided: Only the defined host names in the list are used for this TLS certificate, not the ones defined in the certificate
- The list of host names is empty (wildcard behavior): In this case not a general wildcard is used. Instead, only the host names in the ingress are used as the wildcard. A wildcard TLS may only be used without any other TLS definitions in this ingress.
I think there is a path to implement this feature in the future as this behavior is a special case of the already implemented one in the traefik internals. (https://github.com/containous/traefik/blob/c446c291d9dc49895e69c91e51a054778c71f21a/tls/certificate.go#L150-L154 would be the default behavior, but it can be overwritten by the provider)
I have no clue what so ever how to pass this information from a provider to the internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, after doing a bit of research, there does not seem to be any implementation outside of ingress controller annotations to handle the failures of any of the components of an ingress. It appears that it is all-or-nothing, where the tls section of the spec is parsed in parallel with the rules section of the spec, leaving the annotations to handle the relationship between http/rules and tls sections.
Therefore, I would now be more inclined to just fail/skip loading the entire ingress on an error loading any portion of the tls section of the spec, since there is no way (without going into all the middlewares) to know how important the separate tls sections are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gopenguin thanks for the clarification with regards to the wildcard matter. As long as we clearly document the constraints, I'm fine with it.
In the light of @dtomcej's latest comment: could you please change the logic so that we skip illegal Ingress definitions? AFAICS, that is the last point that keeps us from merging this PR.
} | ||
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, ", ")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this a lot 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incredible work!
LGTM
@@ -174,6 +175,13 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) | |||
continue | |||
} | |||
|
|||
tlsConfigs, err := getTLSConfigurations(i, k8sClient) | |||
if err != nil { | |||
log.Errorf("Error configuring TLS for ingress %s/%s: %v", i.Namespace, i.Name, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, my IDE is telling me that this line is supposedly missing code coverage. Are we possibly missing a test to validate that we actually skip the Ingress if getTLSConfigurations
returns an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you are right. This case isn't covered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM -- thanks a ton @gopenguin for this super useful PR. 🎉
@gopenguin our bot cannot proceed due to a merge conflict. Would you mind resolving it manually? |
@timoreimann Not due to conflicts but to the missing milestone. |
It was a pleasure to contribute to this awesome project. Thanks for your patience and till next time. |
What does this PR do?
Enable Traefik to read the k8s secret defined in a k8s ingress resource via the tls property.
Motivation
This allows a k8s user to easily setup a tls terminated endpoint
More
Additional Notes
This is my first try on this. I would be pleased to get your opinion on this. This is the implementation to #2438