Add support for fetching k8s Ingress TLS data from secrets

This commit is contained in:
gopenguin 2018-01-08 00:36:03 +01:00 committed by Traefiker
parent 9b3750320b
commit 8327dd0c0b
7 changed files with 495 additions and 3 deletions

View file

@ -837,7 +837,18 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend
[frontends."{{$frontendName}}".routes."{{$routeName}}"] [frontends."{{$frontendName}}".routes."{{$routeName}}"]
rule = "{{$route.Rule}}" rule = "{{$route.Rule}}"
{{end}} {{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) { func templatesKubernetesTmplBytes() ([]byte, error) {
return _templatesKubernetesTmpl, nil return _templatesKubernetesTmpl, nil

View file

@ -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. 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 ## Basic Authentication
It's possible to add additional authentication annotations in the Ingress rule. It's possible to add additional authentication annotations in the Ingress rule.

View file

@ -3,6 +3,7 @@ package kubernetes
import ( import (
"testing" "testing"
"github.com/containous/traefik/tls"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/stretchr/testify/assert" "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 // Test
func TestBuildConfiguration(t *testing.T) { 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) 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"),
},
},
},
} }
} }

View file

@ -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 // Test
func TestBuildIngress(t *testing.T) { func TestBuildIngress(t *testing.T) {
@ -107,7 +124,11 @@ func TestBuildIngress(t *testing.T) {
onePath(iBackend("service2", intstr.FromInt(802))), onePath(iBackend("service2", intstr.FromInt(802))),
), ),
), ),
)) ),
iTLSes(
iTLS("tls-secret", "foo"),
),
)
assert.EqualValues(t, sampleIngress(), i) assert.EqualValues(t, sampleIngress(), i)
} }
@ -164,6 +185,12 @@ func sampleIngress() *v1beta1.Ingress {
}, },
}, },
}, },
TLS: []v1beta1.IngressTLS{
{
Hosts: []string{"foo"},
SecretName: "tls-secret",
},
},
}, },
} }
} }

View file

@ -19,6 +19,7 @@ import (
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/label" "github.com/containous/traefik/provider/label"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/tls"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1" "k8s.io/client-go/pkg/apis/extensions/v1beta1"
@ -174,6 +175,13 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
continue 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 { for _, r := range i.Spec.Rules {
if r.HTTP == nil { if r.HTTP == nil {
log.Warn("Error in ingress: HTTP is nil") log.Warn("Error in ingress: HTTP is nil")
@ -441,6 +449,48 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri
return creds, nil 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 { func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
if len(endpointPorts) > 0 { if len(endpointPorts) > 0 {
//name is optional if there is only one port //name is optional if there is only one port

View file

@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/containous/traefik/provider/label" "github.com/containous/traefik/provider/label"
"github.com/containous/traefik/tls"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
@ -1214,3 +1215,302 @@ func TestBasicAuthInTemplate(t *testing.T) {
t.Fatalf("unexpected credentials: %+v", got) 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)
}
})
}
}

View file

@ -92,4 +92,14 @@
[frontends."{{$frontendName}}".routes."{{$routeName}}"] [frontends."{{$frontendName}}".routes."{{$routeName}}"]
rule = "{{$route.Rule}}" rule = "{{$route.Rule}}"
{{end}} {{end}}
{{end}} {{end}}
{{range $tlsConfiguration := .TLSConfiguration}}
[[tlsConfiguration]]
entryPoints = [{{range $tlsConfiguration.EntryPoints}}
"{{.}}",
{{end}}]
[tlsConfiguration.certificate]
certFile = """{{$tlsConfiguration.Certificate.CertFile}}"""
keyFile = """{{$tlsConfiguration.Certificate.KeyFile}}"""
{{end}}