Skip to content

Commit a1253c4

Browse files
hjiaweiclaude
andcommitted
logcollector: user CA for Splunk HEC, watch the CA ConfigMaps
The Splunk output verifies https HEC endpoints against the trusted bundle, but nothing put a private CA there — a self-hosted Splunk with a self-signed certificate could not be used since the splunk-public- certificate secret was removed (#2545). Accept one the same way syslog does: a splunk-ca ConfigMap in tigera-operator (key tls.crt), folded into fluent-bit's bundle. Also watch the syslog-ca and splunk-ca ConfigMaps: previously a created or rotated CA sat unnoticed until an unrelated event triggered a reconcile. Validated on a live cluster: the Splunk e2e forwards flow logs to an https HEC endpoint serving Splunk's self-signed certificate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a06eca9 commit a1253c4

3 files changed

Lines changed: 78 additions & 9 deletions

File tree

pkg/controller/logcollector/logcollector_controller.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ func add(mgr manager.Manager, c ctrlruntime.Controller) error {
142142
return fmt.Errorf("logcollector-controller failed to watch ConfigMap %s: %v", rlogcollector.FluentBitFilterConfigMapName, err)
143143
}
144144

145+
// Watch the user-supplied CA ConfigMaps so creating or rotating a syslog or
146+
// Splunk CA takes effect without waiting for an unrelated reconcile.
147+
for _, caConfigMap := range []string{rlogcollector.SyslogCAConfigMapName, rlogcollector.SplunkCAConfigMapName} {
148+
if err = utils.AddConfigMapWatch(c, caConfigMap, common.OperatorNamespace(), &handler.EnqueueRequestForObject{}); err != nil {
149+
return fmt.Errorf("logcollector-controller failed to watch ConfigMap %s: %v", caConfigMap, err)
150+
}
151+
}
152+
145153
// Watch the rendered configuration ConfigMaps so tampering with them
146154
// triggers a reconcile that restores the rendered content.
147155
for _, configMapName := range []string{
@@ -524,7 +532,7 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile
524532
var useSyslogCertificate bool
525533
if instance.Spec.AdditionalStores != nil {
526534
if instance.Spec.AdditionalStores.Syslog != nil && instance.Spec.AdditionalStores.Syslog.Encryption == operatorv1.EncryptionTLS {
527-
syslogCert, err := getSysLogCertificate(r.client)
535+
syslogCert, err := getUserCACertificate(r.client, rlogcollector.SyslogCAConfigMapName)
528536
if err != nil {
529537
r.status.SetDegraded(operatorv1.ResourceReadError, "Error loading Syslog certificate", err, reqLogger)
530538
return reconcile.Result{}, err
@@ -534,6 +542,22 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile
534542
trustedBundle.AddCertificates(syslogCert)
535543
}
536544
}
545+
// The Splunk output verifies https HEC endpoints against the trusted
546+
// bundle, so a user CA for a self-hosted Splunk rides the same way as
547+
// the syslog one. Plain-http endpoints do no verification, so like
548+
// syslog's TLS gate the CA is only loaded for https.
549+
if splunk := instance.Spec.AdditionalStores.Splunk; splunk != nil {
550+
if proto, _, _, err := url.ParseEndpoint(splunk.Endpoint); err == nil && proto == "https" {
551+
splunkCert, err := getUserCACertificate(r.client, rlogcollector.SplunkCAConfigMapName)
552+
if err != nil {
553+
r.status.SetDegraded(operatorv1.ResourceReadError, "Error loading Splunk certificate", err, reqLogger)
554+
return reconcile.Result{}, err
555+
}
556+
if splunkCert != nil {
557+
trustedBundle.AddCertificates(splunkCert)
558+
}
559+
}
560+
}
537561
}
538562

539563
if instance.Spec.AdditionalStores != nil {
@@ -872,24 +896,25 @@ func getEksCloudwatchLogConfig(client client.Client, interval int32, region, gro
872896
}, nil
873897
}
874898

875-
func getSysLogCertificate(client client.Client) (certificatemanagement.CertificateInterface, error) {
899+
// getUserCACertificate reads an optional user-supplied CA from the named
900+
// ConfigMap in the operator namespace (key tls.crt), for stores whose TLS
901+
// endpoint is not signed by a publicly trusted CA.
902+
func getUserCACertificate(client client.Client, name string) (certificatemanagement.CertificateInterface, error) {
876903
cm := &corev1.ConfigMap{}
877904
cmNamespacedName := types.NamespacedName{
878-
Name: rlogcollector.SyslogCAConfigMapName,
905+
Name: name,
879906
Namespace: common.OperatorNamespace(),
880907
}
881908
if err := client.Get(context.Background(), cmNamespacedName, cm); err != nil {
882909
if errors.IsNotFound(err) {
883-
log.Info(fmt.Sprintf("ConfigMap %q is not found, assuming syslog's certificate is signed by publicly trusted CA", rlogcollector.SyslogCAConfigMapName))
910+
log.Info(fmt.Sprintf("ConfigMap %q is not found, assuming the endpoint's certificate is signed by publicly trusted CA", name))
884911
return nil, nil
885912
}
886-
return nil, fmt.Errorf("failed to read ConfigMap %q: %s", rlogcollector.SyslogCAConfigMapName, err)
913+
return nil, fmt.Errorf("failed to read ConfigMap %q: %s", name, err)
887914
}
888915
if len(cm.Data[corev1.TLSCertKey]) == 0 {
889-
log.Info(fmt.Sprintf("ConfigMap %q does not have a field named %q, assuming syslog's certificate is signed by publicly trusted CA", rlogcollector.SyslogCAConfigMapName, corev1.TLSCertKey))
916+
log.Info(fmt.Sprintf("ConfigMap %q does not have a field named %q, assuming the endpoint's certificate is signed by publicly trusted CA", name, corev1.TLSCertKey))
890917
return nil, nil
891918
}
892-
syslogCert := certificatemanagement.NewCertificate(rlogcollector.SyslogCAConfigMapName, common.OperatorNamespace(), []byte(cm.Data[corev1.TLSCertKey]), nil)
893-
894-
return syslogCert, nil
919+
return certificatemanagement.NewCertificate(name, common.OperatorNamespace(), []byte(cm.Data[corev1.TLSCertKey]), nil), nil
895920
}

pkg/controller/logcollector/logcollector_controller_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,49 @@ var _ = Describe("LogCollector controller tests", func() {
494494
Expect(conf).To(ContainSubstring(`"splunk_token": "${SPLUNK_HEC_TOKEN}"`))
495495
})
496496

497+
It("renders a user-supplied Splunk CA into fluent-bit's bundle", func() {
498+
caPEM := "-----BEGIN CERTIFICATE-----\nsplunk-user-ca\n-----END CERTIFICATE-----"
499+
Expect(c.Create(ctx, &corev1.ConfigMap{
500+
ObjectMeta: metav1.ObjectMeta{Name: rlogcollector.SplunkCAConfigMapName, Namespace: common.OperatorNamespace()},
501+
Data: map[string]string{corev1.TLSCertKey: caPEM},
502+
})).NotTo(HaveOccurred())
503+
504+
_, err := r.Reconcile(ctx, reconcile.Request{})
505+
Expect(err).ShouldNot(HaveOccurred())
506+
507+
bundle := corev1.ConfigMap{
508+
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
509+
ObjectMeta: metav1.ObjectMeta{Name: "calico-fluent-bit-ca-bundle-system-certs", Namespace: render.LogCollectorNamespace},
510+
}
511+
Expect(test.GetResource(c, &bundle)).To(BeNil())
512+
Expect(bundle.Data["tigera-ca-bundle.crt"]).To(ContainSubstring(caPEM))
513+
})
514+
515+
It("does not load the Splunk CA for a plain-http endpoint", func() {
516+
lc := operatorv1.LogCollector{
517+
TypeMeta: metav1.TypeMeta{Kind: "LogCollector", APIVersion: "operator.tigera.io/v1"},
518+
ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"},
519+
}
520+
Expect(test.GetResource(c, &lc)).To(BeNil())
521+
lc.Spec.AdditionalStores.Splunk.Endpoint = "http://localhost:1234"
522+
Expect(c.Update(ctx, &lc)).NotTo(HaveOccurred())
523+
caPEM := "-----BEGIN CERTIFICATE-----\nsplunk-user-ca\n-----END CERTIFICATE-----"
524+
Expect(c.Create(ctx, &corev1.ConfigMap{
525+
ObjectMeta: metav1.ObjectMeta{Name: rlogcollector.SplunkCAConfigMapName, Namespace: common.OperatorNamespace()},
526+
Data: map[string]string{corev1.TLSCertKey: caPEM},
527+
})).NotTo(HaveOccurred())
528+
529+
_, err := r.Reconcile(ctx, reconcile.Request{})
530+
Expect(err).ShouldNot(HaveOccurred())
531+
532+
bundle := corev1.ConfigMap{
533+
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
534+
ObjectMeta: metav1.ObjectMeta{Name: "calico-fluent-bit-ca-bundle-system-certs", Namespace: render.LogCollectorNamespace},
535+
}
536+
Expect(test.GetResource(c, &bundle)).To(BeNil())
537+
Expect(bundle.Data["tigera-ca-bundle.crt"]).NotTo(ContainSubstring(caPEM))
538+
})
539+
497540
Context("Disable feature via license", func() {
498541
BeforeEach(func() {
499542
By("Deleting the previous license")

pkg/render/logcollector/logcollector.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const (
7373
SysLogPublicCertKey = "ca-bundle.crt"
7474
SysLogPublicCAPath = SysLogPublicCADir + SysLogPublicCertKey
7575
SyslogCAConfigMapName = "syslog-ca"
76+
SplunkCAConfigMapName = "splunk-ca"
7677

7778
// Constants for Linseed token volume mounting in managed clusters.
7879
LinseedTokenVolumeName = render.LinseedTokenVolumeName

0 commit comments

Comments
 (0)