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 external IP addresses to certificate #3791

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -213,52 +213,3 @@ PW=$(kubectl get secret "$NAME-es-elastic-user" -o go-template='{{.data.elastic
curl --cacert tls.crt -u elastic:$PW https://$IP:9200/
----

Now you should get this message:

[source,sh]
----
curl: (51) SSL: no alternative certificate subject name matches target host name '35.198.131.115'
----

The request failed because the IP address used to access the service does not match the subject of the certificate, which is set to the service name. We can force curl to temporarily map the IP address to the expected service name by using the `--resolve` flag as follows:

[source,sh]
----
> curl --cacert tls.crt -u elastic:$PW https://$NAME-es-http:9200/ --resolve $NAME-es-http:9200:$IP
{
"name" : "hulk-es-4qk62zd928",
"cluster_name" : "hulk",
"cluster_uuid" : "q6itjqFqRqW576FXF0uohg",
"version" : {...},
"tagline" : "You Know, for Search"
}
----

You can add extra Subject Alternative Name (SAN) entries to the generated certificate by setting the `subjectAltNames` field of the Elasticsearch specification, as illustrated in the example below. Adding the external IP address to the SANs eliminates the need to explicitly configure clients to override the certificate subject comparison.

[source,yaml]
----
spec:
http:
service:
spec:
type: LoadBalancer
tls:
selfSignedCertificate:
subjectAltNames:
- ip: 35.198.131.115
----

You can now reach Elasticsearch without `--resolve`:

[source,sh]
----
> curl --cacert tls.crt -u elastic:$PW https://$IP:9200/
{
"name" : "hulk-es-4qk62zd928",
"cluster_name" : "hulk",
"cluster_uuid" : "q6itjqFqRqW576FXF0uohg",
"version" : {...},
"tagline" : "You Know, for Search"
}
----
3 changes: 2 additions & 1 deletion pkg/controller/common/certificates/http_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,11 @@ func createValidatedHTTPCertificateTemplate(
certCommonName, // eg. clusterName-es-http.default.es.local
shortName, // eg. clusterName-es-http
}
var ipAddresses []net.IP
var ipAddresses []net.IP //nolint:prealloc

for _, svc := range svcs {
dnsNames = append(dnsNames, k8s.GetServiceDNSName(svc)...)
ipAddresses = append(ipAddresses, k8s.GetServiceIPAddresses(svc)...)
}

if selfSignedCerts := tls.SelfSignedCertificate; selfSignedCerts != nil {
Expand Down
38 changes: 36 additions & 2 deletions pkg/utils/k8s/k8sutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package k8s

import (
"fmt"
"net"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -14,6 +15,8 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"

netutil "github.com/elastic/cloud-on-k8s/pkg/utils/net"
)

// ToObjectMeta returns an ObjectMeta based on the given NamespacedName.
Expand Down Expand Up @@ -61,12 +64,43 @@ func PodNames(pods []corev1.Pod) []string {
return names
}

// GetServiceDNSName returns the fully qualified DNS name for a service
// GetServiceDNSName returns the fully qualified DNS name for a service along with any external names provided by ingresses.
func GetServiceDNSName(svc corev1.Service) []string {
return []string{
names := []string{
fmt.Sprintf("%s.%s.svc", svc.Name, svc.Namespace),
fmt.Sprintf("%s.%s", svc.Name, svc.Namespace),
}

if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
for _, ingress := range svc.Status.LoadBalancer.Ingress {
if ingress.Hostname != "" {
names = append(names, ingress.Hostname)
}
}
}

return names
}

func GetServiceIPAddresses(svc corev1.Service) []net.IP {
var ipAddrs []net.IP

if len(svc.Spec.ExternalIPs) > 0 {
ipAddrs = make([]net.IP, len(svc.Spec.ExternalIPs))
for i, externalIP := range svc.Spec.ExternalIPs {
ipAddrs[i] = netutil.IPToRFCForm(net.ParseIP(externalIP))
}
}

if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
for _, ingress := range svc.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
ipAddrs = append(ipAddrs, netutil.IPToRFCForm(net.ParseIP(ingress.IP)))
}
}
}

return ipAddrs
}

// EmitErrorEvent emits an event if the error is report-worthy
Expand Down
63 changes: 63 additions & 0 deletions pkg/utils/k8s/k8sutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package k8s

import (
"net"
"testing"

"github.com/go-test/deep"
Expand All @@ -13,6 +14,8 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

netutil "github.com/elastic/cloud-on-k8s/pkg/utils/net"
)

func TestToObjectMeta(t *testing.T) {
Expand Down Expand Up @@ -47,6 +50,27 @@ func TestGetServiceDNSName(t *testing.T) {
},
want: []string{"test-name.test-ns.svc", "test-name.test-ns"},
},
{
name: "load balancer service",
args: args{
svc: corev1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test-ns", Name: "test-name"},
Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer},
Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "mysvc.lb"}}}},
},
},
want: []string{"test-name.test-ns.svc", "test-name.test-ns", "mysvc.lb"},
},
{
name: "load balancer service (no status)",
args: args{
svc: corev1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test-ns", Name: "test-name"},
Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer},
},
},
want: []string{"test-name.test-ns.svc", "test-name.test-ns"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -57,6 +81,45 @@ func TestGetServiceDNSName(t *testing.T) {
}
}

func TestGetServiceIPAddresses(t *testing.T) {
testCases := []struct {
name string
svc corev1.Service
want []net.IP
}{
{
name: "ClusterIP service",
svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP}},
want: nil,
},
{
name: "NodePort service with external IP addresses",
svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort, ExternalIPs: []string{"1.2.3.4", "2001:db8:a0b:12f0::1"}}},
want: []net.IP{netutil.IPToRFCForm(net.ParseIP("1.2.3.4")), netutil.IPToRFCForm(net.ParseIP("2001:db8:a0b:12f0::1"))},
},
{
name: "LoadBalancer service",
svc: corev1.Service{
Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer},
Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "1.2.3.4"}}}},
},
want: []net.IP{netutil.IPToRFCForm(net.ParseIP("1.2.3.4"))},
},
{
name: "LoadBalancer service (no status)",
svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}},
want: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
have := GetServiceIPAddresses(tc.svc)
require.Equal(t, tc.want, have)
})
}
}

func TestOverrideControllerReference(t *testing.T) {

ownerRefFixture := func(name string, controller bool) metav1.OwnerReference {
Expand Down