Skip to content

Commit 8056d50

Browse files
committed
feat: thanos querier to thanos sidecar mTLS
1 parent 648c091 commit 8056d50

File tree

9 files changed

+375
-22
lines changed

9 files changed

+375
-22
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/go-logr/logr v1.4.2
77
github.com/google/go-cmp v0.7.0
88
github.com/openshift/api v0.0.0-20240404200104-96ed2d49b255
9+
github.com/openshift/library-go v0.0.0-20240216151214-738f3fa4ccf8
910
github.com/perses/perses v0.50.1
1011
github.com/perses/perses-operator v0.1.2
1112
github.com/pkg/errors v0.9.1

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB
5353
github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps=
5454
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
5555
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
56-
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
57-
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
56+
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
57+
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
5858
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
5959
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
6060
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
@@ -215,6 +215,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
215215
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
216216
github.com/openshift/api v0.0.0-20240404200104-96ed2d49b255 h1:OPEl/rl/Bt8soLkMUex9PZu9PJB59VPFnaPh/n1Pb3I=
217217
github.com/openshift/api v0.0.0-20240404200104-96ed2d49b255/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4=
218+
github.com/openshift/library-go v0.0.0-20240216151214-738f3fa4ccf8 h1:dKtHGYiOwl0DKZEWBW4MFWFS6IYW02AVD1WSuUAVwEo=
219+
github.com/openshift/library-go v0.0.0-20240216151214-738f3fa4ccf8/go.mod h1:ePlaOqUiPplRc++6aYdMe+2FmXb2xTNS9Nz5laG2YmI=
218220
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
219221
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
220222
github.com/perses/common v0.26.0 h1:szF3GFTUgsCts3VYU3QY9OfgnYerjzHl9bo9pk4ZGyM=

pkg/assets/certificate_generator.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package assets
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/x509"
6+
"fmt"
7+
"math/big"
8+
"time"
9+
10+
"github.com/go-logr/logr"
11+
"github.com/openshift/library-go/pkg/crypto"
12+
v1 "k8s.io/api/core/v1"
13+
"k8s.io/apimachinery/pkg/util/sets"
14+
"k8s.io/apiserver/pkg/authentication/user"
15+
)
16+
17+
const certificateLifetime = time.Duration(crypto.DefaultCertificateLifetimeInDays) * 24 * time.Hour
18+
const GRPCSecretName = "thanos-grpc-secret"
19+
20+
// Taken from
21+
// https://github.com/openshift/library-go/blob/08c2fd1b452520da35ad210930ea9d100545589a/pkg/operator/certrotation/signer.go#L68-L86
22+
// without refresh time handling. We just take care of rotation if we reach 1/5 of the validity timespan before expiration.
23+
func needsNewCert(notBefore, notAfter time.Time, now func() time.Time) bool {
24+
maxWait := notAfter.Sub(notBefore) / 5
25+
latestTime := notAfter.Add(-maxWait)
26+
return now().After(latestTime)
27+
}
28+
29+
// Taken from
30+
// https://github.com/openshift/cluster-monitoring-operator/blob/765d0b0369b176a5997d787b6710783437172879/pkg/manifests/tls.go#L113
31+
func RotateGRPCSecret(s *v1.Secret, logger logr.Logger) (bool, error) {
32+
var (
33+
curCA, newCA *crypto.CA
34+
curCABytes, crtPresent = s.Data["ca.crt"]
35+
curCAKeyBytes, keyPresent = s.Data["ca.key"]
36+
rotate = !crtPresent || !keyPresent
37+
)
38+
39+
if crtPresent && keyPresent {
40+
var err error
41+
curCA, err = crypto.GetCAFromBytes(curCABytes, curCAKeyBytes)
42+
if err != nil {
43+
logger.Info(fmt.Sprintf("generating a new CA due to error reading CA: %v", err))
44+
rotate = true
45+
} else if needsNewCert(curCA.Config.Certs[0].NotBefore, curCA.Config.Certs[0].NotAfter, time.Now) {
46+
logger.Info("generating new CA, because the current one is older than 1/5 of it validity timestamp")
47+
rotate = true
48+
}
49+
}
50+
51+
if !rotate {
52+
return rotate, nil
53+
}
54+
55+
if curCA == nil {
56+
newCAConfig, err := crypto.MakeSelfSignedCAConfig(
57+
fmt.Sprintf("%s@%d", "openshift-cluster-monitoring", time.Now().Unix()),
58+
crypto.DefaultCertificateLifetimeInDays,
59+
)
60+
if err != nil {
61+
return rotate, fmt.Errorf("error generating self signed CA: %w", err)
62+
}
63+
64+
newCA = &crypto.CA{
65+
SerialGenerator: &crypto.RandomSerialGenerator{},
66+
Config: newCAConfig,
67+
}
68+
} else {
69+
template := curCA.Config.Certs[0]
70+
now := time.Now()
71+
template.NotBefore = now.Add(-1 * time.Second)
72+
template.NotAfter = now.Add(certificateLifetime)
73+
template.SerialNumber = template.SerialNumber.Add(template.SerialNumber, big.NewInt(1))
74+
75+
newCACert, err := createCertificate(template, template, template.PublicKey, curCA.Config.Key)
76+
if err != nil {
77+
return rotate, fmt.Errorf("error rotating CA: %w", err)
78+
}
79+
80+
newCA = &crypto.CA{
81+
SerialGenerator: &crypto.RandomSerialGenerator{},
82+
Config: &crypto.TLSCertificateConfig{
83+
Certs: []*x509.Certificate{newCACert},
84+
Key: curCA.Config.Key,
85+
},
86+
}
87+
}
88+
89+
newCABytes, newCAKeyBytes, err := newCA.Config.GetPEMBytes()
90+
if err != nil {
91+
return rotate, fmt.Errorf("error getting PEM bytes from CA: %w", err)
92+
}
93+
94+
s.Data["ca.crt"] = newCABytes
95+
s.Data["ca.key"] = newCAKeyBytes
96+
97+
{
98+
cfg, err := newCA.MakeClientCertificateForDuration(
99+
&user.DefaultInfo{
100+
Name: "thanos-querier",
101+
},
102+
time.Duration(crypto.DefaultCertificateLifetimeInDays)*24*time.Hour,
103+
)
104+
if err != nil {
105+
return rotate, fmt.Errorf("error making client certificate: %w", err)
106+
}
107+
108+
crt, key, err := cfg.GetPEMBytes()
109+
if err != nil {
110+
return rotate, fmt.Errorf("error getting PEM bytes for thanos querier client certificate: %w", err)
111+
}
112+
s.Data["thanos-querier-client.crt"] = crt
113+
s.Data["thanos-querier-client.key"] = key
114+
}
115+
116+
{
117+
cfg, err := newCA.MakeServerCert(
118+
sets.NewString("prometheus-grpc"),
119+
crypto.DefaultCertificateLifetimeInDays,
120+
)
121+
if err != nil {
122+
return rotate, fmt.Errorf("error making server certificate: %w", err)
123+
}
124+
125+
crt, key, err := cfg.GetPEMBytes()
126+
if err != nil {
127+
return rotate, fmt.Errorf("error getting PEM bytes for prometheus-k8s server certificate: %w", err)
128+
}
129+
s.Data["prometheus-server.crt"] = crt
130+
s.Data["prometheus-server.key"] = key
131+
}
132+
133+
return rotate, nil
134+
}
135+
136+
// createCertificate creates a new certificate and returns it in x509.Certificate form.
137+
func createCertificate(template, parent *x509.Certificate, pub, priv interface{}) (*x509.Certificate, error) {
138+
rawCert, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv)
139+
if err != nil {
140+
return nil, fmt.Errorf("error creating certificate: %w", err)
141+
}
142+
parsedCerts, err := x509.ParseCertificates(rawCert)
143+
if err != nil {
144+
return nil, fmt.Errorf("error parsing certificate: %w", err)
145+
}
146+
return parsedCerts[0], nil
147+
}

pkg/controllers/monitoring/monitoring-stack/components.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"k8s.io/utils/ptr"
1515

1616
stack "github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1"
17+
"github.com/rhobs/observability-operator/pkg/assets"
1718
"github.com/rhobs/observability-operator/pkg/reconciler"
1819
)
1920

@@ -49,6 +50,7 @@ func stackComponentReconcilers(
4950
thanos ThanosConfiguration,
5051
prometheus PrometheusConfiguration,
5152
alertmanager AlertmanagerConfiguration,
53+
tlsHashes map[string]string,
5254
) []reconciler.Reconciler {
5355
prometheusName := ms.Name + "-prometheus"
5456
alertmanagerName := ms.Name + "-alertmanager"
@@ -64,7 +66,7 @@ func stackComponentReconcilers(
6466
reconciler.NewUpdater(newPrometheus(ms, prometheusName,
6567
additionalScrapeConfigsSecretName,
6668
instanceSelectorKey, instanceSelectorValue,
67-
thanos, prometheus), ms),
69+
thanos, prometheus, tlsHashes), ms),
6870
reconciler.NewUpdater(newPrometheusService(ms, instanceSelectorKey, instanceSelectorValue), ms),
6971
reconciler.NewUpdater(newThanosSidecarService(ms, instanceSelectorKey, instanceSelectorValue), ms),
7072
reconciler.NewOptionalUpdater(newPrometheusPDB(ms, instanceSelectorKey, instanceSelectorValue), ms,
@@ -135,6 +137,7 @@ func newPrometheus(
135137
instanceSelectorValue string,
136138
thanosCfg ThanosConfiguration,
137139
prometheusCfg PrometheusConfiguration,
140+
tlsHashes map[string]string,
138141
) *monv1.Prometheus {
139142
prometheusSelector := ms.Spec.ResourceSelector
140143

@@ -213,12 +216,33 @@ func newPrometheus(
213216
}
214217
return []monv1.EnableFeature{}
215218
}(),
219+
Volumes: []corev1.Volume{
220+
{
221+
Name: "thanos-tls-assets",
222+
VolumeSource: corev1.VolumeSource{
223+
Secret: &corev1.SecretVolumeSource{
224+
SecretName: assets.GRPCSecretName,
225+
},
226+
},
227+
},
228+
},
216229
},
217230
Retention: ms.Spec.Retention,
218231
RuleSelector: prometheusSelector,
219232
RuleNamespaceSelector: ms.Spec.NamespaceSelector,
220233
Thanos: &monv1.ThanosSpec{
221234
Image: ptr.To(thanosCfg.Image),
235+
GRPCServerTLSConfig: &monv1.TLSConfig{
236+
CAFile: "/etc/thanos/tls-assets/ca.crt",
237+
CertFile: "/etc/thanos/tls-assets/prometheus-server.crt",
238+
KeyFile: "/etc/thanos/tls-assets/prometheus-server.key",
239+
},
240+
VolumeMounts: []corev1.VolumeMount{
241+
{
242+
Name: "thanos-tls-assets",
243+
MountPath: "/etc/thanos/tls-assets",
244+
},
245+
},
222246
},
223247
},
224248
}
@@ -250,6 +274,14 @@ func newPrometheus(
250274
prometheus.Spec.Secrets = append(prometheus.Spec.Secrets, tlsConfig.CertificateAuthority.Name)
251275
}
252276

277+
if len(tlsHashes) > 0 {
278+
tlsAnnotations := map[string]string{}
279+
for name, hash := range tlsHashes {
280+
tlsAnnotations[fmt.Sprintf("monitoring.openshift.io/%s-hash", name)] = hash
281+
}
282+
prometheus.Spec.CommonPrometheusFields.PodMetadata.Annotations = tlsAnnotations
283+
}
284+
253285
if prometheusCfg.Image != "" {
254286
prometheus.Spec.CommonPrometheusFields.Image = ptr.To(prometheusCfg.Image)
255287
}

pkg/controllers/monitoring/monitoring-stack/controller.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ import (
2929
policyv1 "k8s.io/api/policy/v1"
3030
rbacv1 "k8s.io/api/rbac/v1"
3131
"k8s.io/apimachinery/pkg/api/errors"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/apimachinery/pkg/types"
3335
ctrl "sigs.k8s.io/controller-runtime"
3436
"sigs.k8s.io/controller-runtime/pkg/builder"
3537
"sigs.k8s.io/controller-runtime/pkg/client"
3638
"sigs.k8s.io/controller-runtime/pkg/controller"
3739
"sigs.k8s.io/controller-runtime/pkg/predicate"
3840

3941
stack "github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1"
42+
"github.com/rhobs/observability-operator/pkg/assets"
43+
"github.com/rhobs/observability-operator/pkg/controllers/monitoring/utils"
4044
)
4145

4246
type resourceManager struct {
@@ -136,6 +140,42 @@ func RegisterWithManager(mgr ctrl.Manager, opts Options) error {
136140
func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
137141
logger := rm.logger.WithValues("stack", req.NamespacedName)
138142
logger.Info("Reconciling monitoring stack")
143+
144+
gRPCSecret := v1.Secret{
145+
TypeMeta: metav1.TypeMeta{
146+
APIVersion: v1.SchemeGroupVersion.String(),
147+
Kind: "Secret",
148+
},
149+
ObjectMeta: metav1.ObjectMeta{
150+
Name: assets.GRPCSecretName,
151+
Namespace: req.Namespace,
152+
},
153+
Data: map[string][]byte{},
154+
}
155+
err := rm.k8sClient.Get(ctx,
156+
types.NamespacedName{
157+
Name: assets.GRPCSecretName,
158+
Namespace: req.Namespace,
159+
},
160+
&gRPCSecret)
161+
if client.IgnoreNotFound(err) != nil {
162+
return ctrl.Result{}, err
163+
}
164+
165+
rotate, err := assets.RotateGRPCSecret(&gRPCSecret, logger)
166+
if err != nil {
167+
return ctrl.Result{}, err
168+
}
169+
if rotate {
170+
err = rm.k8sClient.Update(ctx, &gRPCSecret)
171+
if errors.IsNotFound(err) {
172+
err = rm.k8sClient.Create(ctx, &gRPCSecret)
173+
}
174+
if err != nil {
175+
return ctrl.Result{}, err
176+
}
177+
}
178+
139179
ms, err := rm.getStack(ctx, req)
140180
if err != nil {
141181
// retry since some error has occured
@@ -180,13 +220,24 @@ func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
180220
return ctrl.Result{}, err
181221
}
182222
}
223+
// querier <---> sidecar mTLS hashes
224+
mTLSSecretKeys := []string{"prometheus-server.key", "prometheus-server.crt", "ca.crt"}
225+
tlsHashes := map[string]string{}
226+
for _, key := range mTLSSecretKeys {
227+
hash, err := utils.HashOfTLSSecret(assets.GRPCSecretName, key, ms.Namespace, rm.k8sClient)
228+
if err != nil {
229+
return ctrl.Result{}, err
230+
}
231+
tlsHashes[fmt.Sprintf("%s-%s", assets.GRPCSecretName, key)] = hash
232+
}
183233

184234
reconcilers := stackComponentReconcilers(ms,
185235
rm.instanceSelectorKey,
186236
rm.instanceSelectorValue,
187237
rm.thanos,
188238
rm.prometheus,
189239
rm.alertmanager,
240+
tlsHashes,
190241
)
191242
for _, reconciler := range reconcilers {
192243
err := reconciler.Reconcile(ctx, rm.k8sClient, rm.scheme)

0 commit comments

Comments
 (0)