Add support for fetching k8s Ingress TLS data from secrets
This commit is contained in:
parent
9b3750320b
commit
8327dd0c0b
7 changed files with 495 additions and 3 deletions
|
@ -837,7 +837,18 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend
|
|||
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
|
||||
rule = "{{$route.Rule}}"
|
||||
{{end}}
|
||||
{{end}}`)
|
||||
{{end}}
|
||||
|
||||
{{range $tlsConfiguration := .TLSConfiguration}}
|
||||
[[tlsConfiguration]]
|
||||
entryPoints = [{{range $tlsConfiguration.EntryPoints}}
|
||||
"{{.}}",
|
||||
{{end}}]
|
||||
[tlsConfiguration.certificate]
|
||||
certFile = """{{$tlsConfiguration.Certificate.CertFile}}"""
|
||||
keyFile = """{{$tlsConfiguration.Certificate.KeyFile}}"""
|
||||
{{end}}
|
||||
`)
|
||||
|
||||
func templatesKubernetesTmplBytes() ([]byte, error) {
|
||||
return _templatesKubernetesTmpl, nil
|
||||
|
|
|
@ -333,6 +333,51 @@ echo "$(minikube ip) traefik-ui.minikube" | sudo tee -a /etc/hosts
|
|||
|
||||
We should now be able to visit [traefik-ui.minikube](http://traefik-ui.minikube) in the browser and view the Træfik Web UI.
|
||||
|
||||
### Add a TLS Certificate to the Ingress
|
||||
|
||||
!!! 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 an HTTPS-protected ingress, you can leverage the TLS feature of the ingress resource.
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: traefik-web-ui
|
||||
namespace: kube-system
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
spec:
|
||||
rules:
|
||||
- host: traefik-ui.minikube
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: traefik-web-ui
|
||||
servicePort: 80
|
||||
tls:
|
||||
secretName: traefik-ui-tls-cert
|
||||
```
|
||||
|
||||
In addition to the modified ingress you need to provide the TLS certificate via a kubernetes secret in the same namespace as the ingress. The following two commands will generate a new certificate and create a secret containing the key and cert files.
|
||||
|
||||
```shell
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=traefik-ui.minikube"
|
||||
kubectl -n kube-system create secret tls traefik-ui-tls-cert --key=tls.key --cert=tls.crt
|
||||
```
|
||||
|
||||
If there are any errors while loading the TLS section of an ingress, the whole ingress will be skipped.
|
||||
|
||||
!!! note
|
||||
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`.
|
||||
|
||||
!!! note
|
||||
The field `hosts` in the TLS configuration is ignored. Instead, the domains provided by the certificate are used for this purpose. It is recommended to not use wildcard certificates as they will match globally.
|
||||
|
||||
## Basic Authentication
|
||||
|
||||
It's possible to add additional authentication annotations in the Ingress rule.
|
||||
|
|
|
@ -3,6 +3,7 @@ package kubernetes
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/tls"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -201,6 +202,39 @@ func route(name string, rule string) func(*types.Route) string {
|
|||
}
|
||||
}
|
||||
|
||||
func tlsConfigurations(opts ...func(*tls.Configuration)) func(*types.Configuration) {
|
||||
return func(c *types.Configuration) {
|
||||
for _, opt := range opts {
|
||||
tlsConf := &tls.Configuration{}
|
||||
opt(tlsConf)
|
||||
c.TLSConfiguration = append(c.TLSConfiguration, tlsConf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tlsConfiguration(opts ...func(*tls.Configuration)) func(*tls.Configuration) {
|
||||
return func(c *tls.Configuration) {
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tlsEntryPoints(entryPoints ...string) func(*tls.Configuration) {
|
||||
return func(c *tls.Configuration) {
|
||||
c.EntryPoints = entryPoints
|
||||
}
|
||||
}
|
||||
|
||||
func certificate(cert string, key string) func(*tls.Configuration) {
|
||||
return func(c *tls.Configuration) {
|
||||
c.Certificate = &tls.Certificate{
|
||||
CertFile: tls.FileOrContent(cert),
|
||||
KeyFile: tls.FileOrContent(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
|
||||
func TestBuildConfiguration(t *testing.T) {
|
||||
|
@ -247,6 +281,12 @@ func TestBuildConfiguration(t *testing.T) {
|
|||
),
|
||||
),
|
||||
),
|
||||
tlsConfigurations(
|
||||
tlsConfiguration(
|
||||
tlsEntryPoints("https"),
|
||||
certificate("certificate", "key"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert.EqualValues(t, sampleConfiguration(), actual)
|
||||
|
@ -335,5 +375,14 @@ func sampleConfiguration() *types.Configuration {
|
|||
},
|
||||
},
|
||||
},
|
||||
TLSConfiguration: []*tls.Configuration{
|
||||
{
|
||||
EntryPoints: []string{"https"},
|
||||
Certificate: &tls.Certificate{
|
||||
CertFile: tls.FileOrContent("certificate"),
|
||||
KeyFile: tls.FileOrContent("key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,23 @@ func iBackend(name string, port intstr.IntOrString) func(*v1beta1.HTTPIngressPat
|
|||
}
|
||||
}
|
||||
|
||||
func iTLSes(opts ...func(*v1beta1.IngressTLS)) func(*v1beta1.Ingress) {
|
||||
return func(i *v1beta1.Ingress) {
|
||||
for _, opt := range opts {
|
||||
iTLS := v1beta1.IngressTLS{}
|
||||
opt(&iTLS)
|
||||
i.Spec.TLS = append(i.Spec.TLS, iTLS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func iTLS(secret string, hosts ...string) func(*v1beta1.IngressTLS) {
|
||||
return func(i *v1beta1.IngressTLS) {
|
||||
i.SecretName = secret
|
||||
i.Hosts = hosts
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
|
||||
func TestBuildIngress(t *testing.T) {
|
||||
|
@ -107,7 +124,11 @@ func TestBuildIngress(t *testing.T) {
|
|||
onePath(iBackend("service2", intstr.FromInt(802))),
|
||||
),
|
||||
),
|
||||
))
|
||||
),
|
||||
iTLSes(
|
||||
iTLS("tls-secret", "foo"),
|
||||
),
|
||||
)
|
||||
|
||||
assert.EqualValues(t, sampleIngress(), i)
|
||||
}
|
||||
|
@ -164,6 +185,12 @@ func sampleIngress() *v1beta1.Ingress {
|
|||
},
|
||||
},
|
||||
},
|
||||
TLS: []v1beta1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo"},
|
||||
SecretName: "tls-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/containous/traefik/provider"
|
||||
"github.com/containous/traefik/provider/label"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/tls"
|
||||
"github.com/containous/traefik/types"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
|
||||
|
@ -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)
|
||||
continue
|
||||
}
|
||||
templateObjects.TLSConfiguration = append(templateObjects.TLSConfiguration, tlsConfigs...)
|
||||
|
||||
for _, r := range i.Spec.Rules {
|
||||
if r.HTTP == nil {
|
||||
log.Warn("Error in ingress: HTTP is nil")
|
||||
|
@ -441,6 +449,48 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri
|
|||
return creds, nil
|
||||
}
|
||||
|
||||
func getTLSConfigurations(ingress *v1beta1.Ingress, k8sClient Client) ([]*tls.Configuration, error) {
|
||||
var tlsConfigs []*tls.Configuration
|
||||
|
||||
for _, t := range ingress.Spec.TLS {
|
||||
tlsSecret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err)
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName)
|
||||
}
|
||||
|
||||
tlsCrtData, tlsCrtExists := tlsSecret.Data["tls.crt"]
|
||||
tlsKeyData, tlsKeyExists := tlsSecret.Data["tls.key"]
|
||||
|
||||
var missingEntries []string
|
||||
if !tlsCrtExists {
|
||||
missingEntries = append(missingEntries, "tls.crt")
|
||||
}
|
||||
if !tlsKeyExists {
|
||||
missingEntries = append(missingEntries, "tls.key")
|
||||
}
|
||||
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, ", "))
|
||||
}
|
||||
|
||||
entryPoints := label.GetSliceStringValue(ingress.Annotations, label.TraefikFrontendEntryPoints)
|
||||
|
||||
tlsConfig := &tls.Configuration{
|
||||
EntryPoints: entryPoints,
|
||||
Certificate: &tls.Certificate{
|
||||
CertFile: tls.FileOrContent(tlsCrtData),
|
||||
KeyFile: tls.FileOrContent(tlsKeyData),
|
||||
},
|
||||
}
|
||||
|
||||
tlsConfigs = append(tlsConfigs, tlsConfig)
|
||||
}
|
||||
|
||||
return tlsConfigs, nil
|
||||
}
|
||||
|
||||
func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
|
||||
if len(endpointPorts) > 0 {
|
||||
//name is optional if there is only one port
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/containous/traefik/provider/label"
|
||||
"github.com/containous/traefik/tls"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
|
@ -1214,3 +1215,302 @@ func TestBasicAuthInTemplate(t *testing.T) {
|
|||
t.Fatalf("unexpected credentials: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSSecretLoad(t *testing.T) {
|
||||
ingresses := []*v1beta1.Ingress{
|
||||
buildIngress(
|
||||
iNamespace("testing"),
|
||||
iAnnotation(label.TraefikFrontendEntryPoints, "ep1,ep2"),
|
||||
iRules(
|
||||
iRule(iHost("example.com"), iPaths(
|
||||
onePath(iBackend("example-com", intstr.FromInt(80))),
|
||||
)),
|
||||
iRule(iHost("example.org"), iPaths(
|
||||
onePath(iBackend("example-org", intstr.FromInt(80))),
|
||||
)),
|
||||
),
|
||||
iTLSes(
|
||||
iTLS("myTlsSecret"),
|
||||
),
|
||||
),
|
||||
buildIngress(
|
||||
iNamespace("testing"),
|
||||
iAnnotation(label.TraefikFrontendEntryPoints, "ep3"),
|
||||
iRules(
|
||||
iRule(iHost("example.fail"), iPaths(
|
||||
onePath(iBackend("example-fail", intstr.FromInt(80))),
|
||||
)),
|
||||
),
|
||||
iTLSes(
|
||||
iTLS("myUndefinedSecret"),
|
||||
),
|
||||
),
|
||||
}
|
||||
services := []*v1.Service{
|
||||
buildService(
|
||||
sName("example-com"),
|
||||
sNamespace("testing"),
|
||||
sUID("1"),
|
||||
sSpec(
|
||||
clusterIP("10.0.0.1"),
|
||||
sType("ClusterIP"),
|
||||
sPorts(sPort(80, "http"))),
|
||||
),
|
||||
buildService(
|
||||
sName("example-org"),
|
||||
sNamespace("testing"),
|
||||
sUID("2"),
|
||||
sSpec(
|
||||
clusterIP("10.0.0.2"),
|
||||
sType("ClusterIP"),
|
||||
sPorts(sPort(80, "http"))),
|
||||
),
|
||||
}
|
||||
secrets := []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "myTlsSecret",
|
||||
UID: "1",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"tls.crt": []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
|
||||
"tls.key": []byte("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
expected := buildConfiguration(
|
||||
backends(
|
||||
backend("example.com",
|
||||
servers(),
|
||||
lbMethod("wrr"),
|
||||
),
|
||||
backend("example.org",
|
||||
servers(),
|
||||
lbMethod("wrr"),
|
||||
),
|
||||
),
|
||||
frontends(
|
||||
frontend("example.com",
|
||||
headers(),
|
||||
entryPoints("ep1", "ep2"),
|
||||
passHostHeader(),
|
||||
routes(
|
||||
route("example.com", "Host:example.com"),
|
||||
),
|
||||
),
|
||||
frontend("example.org",
|
||||
headers(),
|
||||
entryPoints("ep1", "ep2"),
|
||||
passHostHeader(),
|
||||
routes(
|
||||
route("example.org", "Host:example.org"),
|
||||
),
|
||||
),
|
||||
),
|
||||
tlsConfigurations(
|
||||
tlsConfiguration(
|
||||
tlsEntryPoints("ep1", "ep2"),
|
||||
certificate(
|
||||
"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
|
||||
"-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestGetTLSConfigurations(t *testing.T) {
|
||||
testIngressWithoutHostname := buildIngress(
|
||||
iNamespace("testing"),
|
||||
iRules(
|
||||
iRule(iHost("ep1.example.com")),
|
||||
iRule(iHost("ep2.example.com")),
|
||||
),
|
||||
iTLSes(
|
||||
iTLS("test-secret"),
|
||||
),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
ingress *v1beta1.Ingress
|
||||
client Client
|
||||
result []*tls.Configuration
|
||||
errResult string
|
||||
}{
|
||||
{
|
||||
desc: "api client returns error",
|
||||
ingress: testIngressWithoutHostname,
|
||||
client: clientMock{
|
||||
apiSecretError: errors.New("api secret error"),
|
||||
},
|
||||
errResult: "failed to fetch secret testing/test-secret: api secret error",
|
||||
},
|
||||
{
|
||||
desc: "api client doesn't find secret",
|
||||
ingress: testIngressWithoutHostname,
|
||||
client: clientMock{},
|
||||
errResult: "secret testing/test-secret does not exist",
|
||||
},
|
||||
{
|
||||
desc: "entry 'tls.crt' in secret missing",
|
||||
ingress: testIngressWithoutHostname,
|
||||
client: clientMock{
|
||||
secrets: []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"tls.key": []byte("tls-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errResult: "secret testing/test-secret is missing the following TLS data entries: tls.crt",
|
||||
},
|
||||
{
|
||||
desc: "entry 'tls.key' in secret missing",
|
||||
ingress: testIngressWithoutHostname,
|
||||
client: clientMock{
|
||||
secrets: []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"tls.crt": []byte("tls-crt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errResult: "secret testing/test-secret is missing the following TLS data entries: tls.key",
|
||||
},
|
||||
{
|
||||
desc: "secret doesn't provide any of the required fields",
|
||||
ingress: testIngressWithoutHostname,
|
||||
client: clientMock{
|
||||
secrets: []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errResult: "secret testing/test-secret is missing the following TLS data entries: tls.crt, tls.key",
|
||||
},
|
||||
{
|
||||
desc: "add certificates to the configuration",
|
||||
ingress: buildIngress(
|
||||
iNamespace("testing"),
|
||||
iRules(
|
||||
iRule(iHost("ep1.example.com")),
|
||||
iRule(iHost("ep2.example.com")),
|
||||
iRule(iHost("ep3.example.com")),
|
||||
),
|
||||
iTLSes(
|
||||
iTLS("test-secret"),
|
||||
iTLS("test-secret"),
|
||||
),
|
||||
),
|
||||
client: clientMock{
|
||||
secrets: []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"tls.crt": []byte("tls-crt"),
|
||||
"tls.key": []byte("tls-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*tls.Configuration{
|
||||
{
|
||||
Certificate: &tls.Certificate{
|
||||
CertFile: tls.FileOrContent("tls-crt"),
|
||||
KeyFile: tls.FileOrContent("tls-key"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Certificate: &tls.Certificate{
|
||||
CertFile: tls.FileOrContent("tls-crt"),
|
||||
KeyFile: tls.FileOrContent("tls-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "pass the endpoints defined in the annotation to the certificate",
|
||||
ingress: buildIngress(
|
||||
iNamespace("testing"),
|
||||
iAnnotation(label.TraefikFrontendEntryPoints, "https,api-secure"),
|
||||
iRules(iRule(iHost("example.com"))),
|
||||
iTLSes(iTLS("test-secret")),
|
||||
),
|
||||
client: clientMock{
|
||||
secrets: []*v1.Secret{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test-secret",
|
||||
Namespace: "testing",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"tls.crt": []byte("tls-crt"),
|
||||
"tls.key": []byte("tls-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []*tls.Configuration{
|
||||
{
|
||||
EntryPoints: []string{"https", "api-secure"},
|
||||
Certificate: &tls.Certificate{
|
||||
CertFile: tls.FileOrContent("tls-crt"),
|
||||
KeyFile: tls.FileOrContent("tls-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tlsConfigs, err := getTLSConfigurations(test.ingress, test.client)
|
||||
|
||||
if test.errResult != "" {
|
||||
assert.EqualError(t, err, test.errResult)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, test.result, tlsConfigs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,3 +93,13 @@
|
|||
rule = "{{$route.Rule}}"
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{range $tlsConfiguration := .TLSConfiguration}}
|
||||
[[tlsConfiguration]]
|
||||
entryPoints = [{{range $tlsConfiguration.EntryPoints}}
|
||||
"{{.}}",
|
||||
{{end}}]
|
||||
[tlsConfiguration.certificate]
|
||||
certFile = """{{$tlsConfiguration.Certificate.CertFile}}"""
|
||||
keyFile = """{{$tlsConfiguration.Certificate.KeyFile}}"""
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue