Skip to content

Commit c1ba906

Browse files
authored
Fix Stack Monitoring with custom certificate without CA (#5310) (#5319)
Stack Monitoring incorrectly assumed that if the monitored Elastic resource has TLS enabled, there is a CA to configure in the input section of the Metricbeat config. This overlooked that you can have a certificate issued by a well-known CA, so you don't always provide a CA when TLS is enabled. This is fixed by differentiating between isSSL/isTLS and HasCA.
1 parent 159e072 commit c1ba906

File tree

7 files changed

+173
-30
lines changed

7 files changed

+173
-30
lines changed

pkg/controller/common/certificates/ca.go

+21
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package certificates
66

77
import (
8+
"context"
89
"crypto"
910
cryptorand "crypto/rand"
1011
"crypto/rsa"
@@ -14,6 +15,11 @@ import (
1415
"time"
1516

1617
"github.com/pkg/errors"
18+
corev1 "k8s.io/api/core/v1"
19+
"k8s.io/apimachinery/pkg/types"
20+
21+
"github.com/elastic/cloud-on-k8s/pkg/controller/common/name"
22+
"github.com/elastic/cloud-on-k8s/pkg/utils/k8s"
1723
)
1824

1925
var (
@@ -124,3 +130,18 @@ func (c *CA) CreateCertificate(
124130

125131
return certData, err
126132
}
133+
134+
// PublicCertsHasCACert returns true if an Elastic resource has a CA (ca.crt) in its public HTTP certs secret given its namer,
135+
// namespace and name.
136+
func PublicCertsHasCACert(client k8s.Client, namer name.Namer, namespace string, name string) (bool, error) {
137+
certsNsn := types.NamespacedName{
138+
Name: PublicCertsSecretName(namer, name),
139+
Namespace: namespace,
140+
}
141+
var certsSecret corev1.Secret
142+
if err := client.Get(context.Background(), certsNsn, &certsSecret); err != nil {
143+
return false, err
144+
}
145+
_, ok := certsSecret.Data[CAFileName]
146+
return ok, nil
147+
}

pkg/controller/common/certificates/ca_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import (
1515

1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"
18+
corev1 "k8s.io/api/core/v1"
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
21+
esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1"
22+
"github.com/elastic/cloud-on-k8s/pkg/utils/k8s"
1823
)
1924

2025
func init() {
@@ -85,3 +90,59 @@ func TestNewSelfSignedCA(t *testing.T) {
8590
require.Equal(t, testRSAPrivateKey, ca.PrivateKey)
8691
require.True(t, ca.Cert.NotBefore.Before(time.Now().Add(2*time.Hour)))
8792
}
93+
94+
func Test_PublicCertsHasCACert(t *testing.T) {
95+
tests := []struct {
96+
name string
97+
secret corev1.Secret
98+
wantErr bool
99+
want bool
100+
}{
101+
{
102+
name: "Happy path: with ca.crt",
103+
secret: corev1.Secret{
104+
ObjectMeta: metav1.ObjectMeta{
105+
Namespace: "ns",
106+
Name: "c1-es-http-certs-public",
107+
},
108+
Data: map[string][]byte{
109+
CAFileName: []byte("..."),
110+
CertFileName: []byte("..."),
111+
},
112+
},
113+
wantErr: false,
114+
want: true,
115+
},
116+
{
117+
name: "Happy path, without ca.crt",
118+
secret: corev1.Secret{
119+
ObjectMeta: metav1.ObjectMeta{
120+
Namespace: "ns",
121+
Name: "c1-es-http-certs-public",
122+
},
123+
Data: map[string][]byte{
124+
CertFileName: []byte("..."),
125+
},
126+
},
127+
wantErr: false,
128+
want: false,
129+
},
130+
{
131+
name: "Error if no certs secret",
132+
wantErr: true,
133+
want: false,
134+
},
135+
}
136+
137+
for _, tt := range tests {
138+
t.Run(tt.name, func(t *testing.T) {
139+
got, err := PublicCertsHasCACert(k8s.NewFakeClient(&tt.secret), esv1.ESNamer, "ns", "c1")
140+
if (err != nil) != tt.wantErr {
141+
t.Errorf("PublicCertsHasCACert() error = %v, wantErr = %v", err, tt.wantErr)
142+
}
143+
if got != tt.want {
144+
t.Errorf("PublicCertsHasCACert() got = %v, want = %v", got, tt.want)
145+
}
146+
})
147+
}
148+
}

pkg/controller/common/stackmon/config.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ type inputConfigData struct {
160160
Username string
161161
Password string
162162
IsSSL bool
163-
SSLPath string
164-
SSLMode string
163+
HasCA bool
164+
CAPath string
165165
}
166166

167167
// buildMetricbeatBaseConfig builds the base configuration for Metricbeat with the Elasticsearch or Kibana modules used
@@ -181,23 +181,32 @@ func buildMetricbeatBaseConfig(
181181
return "", nil, err
182182
}
183183

184+
hasCA := false
185+
if isTLS {
186+
var err error
187+
hasCA, err = certificates.PublicCertsHasCACert(client, namer, nsn.Namespace, nsn.Name)
188+
if err != nil {
189+
return "", nil, err
190+
}
191+
}
192+
184193
configData := inputConfigData{
185-
URL: url,
186194
Username: user.MonitoringUserName,
187195
Password: password,
188-
IsSSL: isTLS,
196+
URL: url, // Metricbeat in the sidecar connects to the monitored resource using `localhost`
197+
IsSSL: isTLS, // enable SSL configuration based on whether the monitored resource has TLS enabled
198+
HasCA: hasCA, // the CA is optional to support custom certificate issued by a well-known CA, so without provided CA to configure
189199
}
190200

191201
var caVolume volume.VolumeLike
192-
if configData.IsSSL {
202+
if configData.HasCA {
193203
caVolume = volume.NewSecretVolumeWithMountPath(
194204
certificates.PublicCertsSecretName(namer, nsn.Name),
195205
fmt.Sprintf("%s-local-ca", string(associationType)),
196206
fmt.Sprintf("/mnt/elastic-internal/%s/%s/%s/certs", string(associationType), nsn.Namespace, nsn.Name),
197207
)
198208

199-
configData.SSLPath = filepath.Join(caVolume.VolumeMount().MountPath, certificates.CAFileName)
200-
configData.SSLMode = "certificate"
209+
configData.CAPath = filepath.Join(caVolume.VolumeMount().MountPath, certificates.CAFileName)
201210
}
202211

203212
// render the config template with the config data

pkg/controller/common/stackmon/config_test.go

+49-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/stretchr/testify/assert"
1111
corev1 "k8s.io/api/core/v1"
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
1314
"k8s.io/apimachinery/pkg/types"
1415

1516
"github.com/elastic/cloud-on-k8s/pkg/controller/common/name"
@@ -18,53 +19,86 @@ import (
1819

1920
func TestBuildMetricbeatBaseConfig(t *testing.T) {
2021
tests := []struct {
21-
name string
22-
isTLS bool
23-
baseConfig string
22+
name string
23+
isTLS bool
24+
certsSecret *corev1.Secret
25+
hasCA bool
26+
baseConfig string
2427
}{
2528
{
26-
name: "with tls",
29+
name: "with TLS and a CA",
2730
isTLS: true,
31+
certsSecret: &corev1.Secret{
32+
ObjectMeta: metav1.ObjectMeta{Name: "name-es-http-certs-public", Namespace: "namespace"},
33+
Data: map[string][]byte{
34+
"tls.crt": []byte("1234567890"),
35+
"ca.crt": []byte("1234567890"),
36+
},
37+
},
2838
baseConfig: `
2939
hosts: ["scheme://localhost:1234"]
3040
username: elastic-internal-monitoring
3141
password: 1234567890
32-
ssl.certificate_authorities: ["/mnt/elastic-internal/xx-monitoring/namespace/name/certs/ca.crt"]
42+
ssl.enabled: true
43+
ssl.verification_mode: "certificate"
44+
ssl.certificate_authorities: ["/mnt/elastic-internal/xx-monitoring/namespace/name/certs/ca.crt"]`,
45+
},
46+
{
47+
name: "with TLS and no CA",
48+
isTLS: true,
49+
certsSecret: &corev1.Secret{
50+
ObjectMeta: metav1.ObjectMeta{Name: "name-es-http-certs-public", Namespace: "namespace"},
51+
Data: map[string][]byte{
52+
"tls.crt": []byte("1234567890"),
53+
},
54+
},
55+
baseConfig: `
56+
hosts: ["scheme://localhost:1234"]
57+
username: elastic-internal-monitoring
58+
password: 1234567890
59+
ssl.enabled: true
3360
ssl.verification_mode: "certificate"`,
3461
},
3562
{
36-
name: "without tls",
63+
name: "without TLS",
3764
isTLS: false,
3865
baseConfig: `
3966
hosts: ["scheme://localhost:1234"]
4067
username: elastic-internal-monitoring
41-
password: 1234567890`,
68+
password: 1234567890
69+
ssl.enabled: false
70+
ssl.verification_mode: "certificate"`,
4271
},
4372
}
44-
4573
baseConfigTemplate := `
4674
hosts: ["{{ .URL }}"]
4775
username: {{ .Username }}
4876
password: {{ .Password }}
49-
{{- if .IsSSL }}
50-
ssl.certificate_authorities: ["{{ .SSLPath }}"]
51-
ssl.verification_mode: "{{ .SSLMode }}"
77+
ssl.enabled: {{ .IsSSL }}
78+
ssl.verification_mode: "certificate"
79+
{{- if .HasCA }}
80+
ssl.certificate_authorities: ["{{ .CAPath }}"]
5281
{{- end }}`
53-
sampleURL := "scheme://localhost:1234"
5482

55-
fakeClient := k8s.NewFakeClient(&corev1.Secret{
83+
sampleURL := "scheme://localhost:1234"
84+
internalUsersSecret := &corev1.Secret{
5685
ObjectMeta: metav1.ObjectMeta{Name: "name-es-internal-users", Namespace: "namespace"},
5786
Data: map[string][]byte{"elastic-internal-monitoring": []byte("1234567890")},
58-
})
87+
}
5988

6089
for _, tc := range tests {
6190
t.Run(tc.name, func(t *testing.T) {
91+
initObjects := []runtime.Object{internalUsersSecret}
92+
if tc.certsSecret != nil {
93+
initObjects = append(initObjects, tc.certsSecret)
94+
}
95+
fakeClient := k8s.NewFakeClient(initObjects...)
6296
baseConfig, _, err := buildMetricbeatBaseConfig(
6397
fakeClient,
6498
"xx-monitoring",
6599
types.NamespacedName{Namespace: "namespace", Name: "name"},
66100
types.NamespacedName{Namespace: "namespace", Name: "name"},
67-
name.NewNamer("xx"),
101+
name.NewNamer("es"),
68102
sampleURL,
69103
tc.isTLS,
70104
baseConfigTemplate,

pkg/controller/elasticsearch/stackmon/metricbeat.tpl.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ metricbeat.modules:
1717
hosts: ["{{ .URL }}"]
1818
username: {{ .Username }}
1919
password: {{ .Password }}
20-
{{- if .IsSSL }}
21-
ssl.certificate_authorities: ["{{ .SSLPath }}"]
22-
ssl.verification_mode: "{{ .SSLMode }}"
20+
ssl.enabled: {{ .IsSSL }}
21+
# The ssl verification_mode is set to `certificate` in the config template to verify that the certificate is signed by a trusted authority,
22+
# but does not perform any hostname verification. This is used when SSL is enabled with or without CA, to support self-signed certificate
23+
# with a custom CA or custom certificates with or without a CA that most likely are not issued for `localhost`.
24+
ssl.verification_mode: "certificate"
25+
{{- if .HasCA }}
26+
ssl.certificate_authorities: ["{{ .CAPath }}"]
2327
{{- end }}
2428

2529
processors:

pkg/controller/kibana/stackmon/metricbeat.tpl.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ metricbeat.modules:
99
hosts: ["{{ .URL }}"]
1010
username: {{ .Username }}
1111
password: {{ .Password }}
12-
{{- if .IsSSL }}
13-
ssl.certificate_authorities: ["{{ .SSLPath }}"]
14-
ssl.verification_mode: "{{ .SSLMode }}"
12+
ssl.enabled: {{ .IsSSL }}
13+
# The ssl verification_mode is set to `certificate` in the config template to verify that the certificate is signed by a trusted authority,
14+
# but does not perform any hostname verification. This is used when SSL is enabled with or without CA, to support self-signed certificate
15+
# with a custom CA or custom certificates with or without a CA that most likely are not issued for `localhost`.
16+
ssl.verification_mode: "certificate"
17+
{{- if .HasCA }}
18+
ssl.certificate_authorities: ["{{ .CAPath }}"]
1519
{{- end }}
1620

1721
processors:

pkg/controller/kibana/stackmon/sidecar_test.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,19 @@ func TestWithMonitoring(t *testing.T) {
4848
}
4949
fakeEsHTTPCertSecret := corev1.Secret{
5050
ObjectMeta: metav1.ObjectMeta{Name: "sample-es-http-certs-public", Namespace: "aerospace"},
51-
Data: map[string][]byte{"ca.crt": []byte("7H1515N074r341C3r71F1C473")},
51+
Data: map[string][]byte{
52+
"tls.crt": []byte("7H1515N074r341C3r71F1C473"),
53+
"ca.crt": []byte("7H1515N074r341C3r71F1C473"),
54+
},
55+
}
56+
fakeKbHTTPCertSecret := corev1.Secret{
57+
ObjectMeta: metav1.ObjectMeta{Name: "sample-kb-http-certs-public", Namespace: "aerospace"},
58+
Data: map[string][]byte{
59+
"tls.crt": []byte("7H1515N074r341C3r71F1C473"),
60+
"ca.crt": []byte("7H1515N074r341C3r71F1C473"),
61+
},
5262
}
53-
fakeClient := k8s.NewFakeClient(&fakeElasticUserSecret, &fakeMetricsBeatUserSecret, &fakeLogsBeatUserSecret, &fakeEsHTTPCertSecret)
63+
fakeClient := k8s.NewFakeClient(&fakeElasticUserSecret, &fakeMetricsBeatUserSecret, &fakeLogsBeatUserSecret, &fakeEsHTTPCertSecret, &fakeKbHTTPCertSecret)
5464

5565
monitoringAssocConf := commonv1.AssociationConf{
5666
AuthSecretName: "sample-observability-monitoring-beat-es-mon-user",

0 commit comments

Comments
 (0)