Skip to content

Conversation

@petrpinkas
Copy link
Member

@petrpinkas petrpinkas commented Oct 29, 2025

User description

https://issues.redhat.com/browse/SECURESIGN-3167

Summary by Sourcery

Enable CTLog operator to self-heal missing or invalid server configuration secrets by validating and regenerating them, supported by a new validation utility and comprehensive e2e recovery tests.

New Features:

  • Introduce automated self-healing for CTLog configuration secrets by detecting and recreating missing or invalid secrets
  • Add IsSecretDataValid utility to validate CTLog config secrets against the expected Trillian backend address

Enhancements:

  • Always revalidate custom server config secrets in the serverConfig handler and trigger recreation on mismatch

Tests:

  • Add end-to-end integration test for CTLog recovery flow, including secret deletion, automatic reconciliation, and service readiness verification with helper functions for secret management

PR Type

Enhancement, Tests


Description

  • Add CTLog config secret validation and self-healing recovery mechanism

  • Implement automatic detection and recreation of missing or invalid config secrets

  • Add integration tests for CTLog recovery scenarios with Trillian address validation

  • Introduce helper functions for config secret management and Trillian address extraction


Diagram Walkthrough

flowchart LR
  A["CTLog CR"] -->|"spec.Trillian"| B["serverConfig.Handle"]
  B -->|"validate secret"| C["IsSecretDataValid"]
  C -->|"missing/invalid"| D["Recreate Secret"]
  D -->|"with correct config"| E["Secret with Trillian Address"]
  E -->|"verify"| F["CTLog Ready"]
  B -->|"test scenarios"| G["Integration Tests"]
  G -->|"validate recovery"| H["Config Restored"]
Loading

File Walkthrough

Relevant files
Enhancement
server_config.go
Add config secret validation and recovery logic                   

internal/controller/ctlog/actions/server_config.go

  • Modified CanHandle() to always validate secret existence and validity
    instead of checking generation
  • Added validation for custom server config secrets with error handling
    for missing or invalid configurations
  • Implemented secret validation logic that checks for missing secrets
    and validates Trillian address configuration
  • Added recovery mechanism to detect and recreate invalid or missing
    config secrets with proper logging and events
+57/-1   
ctlog_config.go
Add secret validation helper function                                       

internal/controller/ctlog/utils/ctlog_config.go

  • Added IsSecretDataValid() function to validate CTLog config secrets
    contain correct Trillian backend address
  • Validates secret data exists, contains required config key, and has
    non-empty configuration
  • Performs string parsing to extract and verify Trillian address from
    config data
  • Used by operator for self-healing to detect missing or invalid
    configurations
+35/-0   
Tests
ctlog_recovery_test.go
Add CTLog recovery integration tests                                         

test/e2e/ctlog_recovery_test.go

  • Comprehensive integration test suite for CTLog recovery and validation
    scenarios
  • Tests config secret creation with correct Trillian address
    configuration
  • Simulates disaster scenario by deleting config secret and verifies
    automatic recreation
  • Validates operator detects missing/invalid secrets and recreates them
    with correct configuration
  • Verifies TreeID preservation and CTLog deployment readiness after
    recovery
+247/-0 
ctlog.go
Add config secret test helper functions                                   

test/e2e/support/tas/ctlog/ctlog.go

  • Added GetConfigSecret() helper to retrieve config secrets by name
  • Added DeleteConfigSecret() helper to delete config secrets for testing
  • Added GetTrillianAddressFromSecret() helper to extract Trillian
    address from config secret data
  • Supports test scenarios for config secret management and validation
+37/-0   

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 29, 2025

Reviewer's Guide

This PR enhances the CTLog operator to self-heal its configuration secret by validating and recreating it when missing or invalid, adds utility and parsing helpers for managing CTLog config secrets in tests and controller logic, and introduces end-to-end integration tests that simulate secret loss and verify operator recovery behavior and TreeID preservation.

Sequence diagram for CTLog config secret validation and recovery

sequenceDiagram
participant Operator
participant Kubernetes
participant Secret
Operator->>Kubernetes: Get CTLog config secret
alt Secret not found
    Operator->>Operator: Log info (missing secret)
    Operator->>Operator: Record event (CTLogConfigMissing)
    Operator->>Kubernetes: Recreate config secret
else Error accessing secret
    Operator->>Operator: Log error
    Operator->>Operator: Record event (CTLogConfigError)
    Operator->>Kubernetes: Recreate config secret
else Secret exists
    Operator->>Secret: Validate secret data
    alt Secret valid
        Operator->>Operator: Continue (no action)
    else Secret invalid
        Operator->>Operator: Log info (invalid secret)
        Operator->>Operator: Record event (CTLogConfigInvalid)
        Operator->>Kubernetes: Recreate config secret
    end
end
Loading

Class diagram for CTLog config secret validation helper

classDiagram
class ctlogUtils {
  +IsSecretDataValid(secretData map[string][]byte, expectedTrillianAddr string) bool
}

ctlogUtils : IsSecretDataValid checks secret data for valid Trillian address
Loading

File-Level Changes

Change Details Files
Enhance server_config handler for CTLog to always validate and self-heal its config secret
  • CanHandle() default now always returns true to trigger validation
  • Added import of apierrors for NotFound check
  • Validate custom secret existence and data key before status update
  • Check existing secret in Status.ServerConfigRef, detect missing or invalid data and emit events
  • Use IsSecretDataValid to decide if secret matches expected Trillian address or must be recreated
internal/controller/ctlog/actions/server_config.go
Add utility functions in test support for config secret management and parsing
  • Implement GetConfigSecret() to fetch a config secret
  • Implement DeleteConfigSecret() to delete a config secret
  • Implement GetTrillianAddressFromSecret() to extract raw config string for address matching
test/e2e/support/tas/ctlog/ctlog.go
Introduce IsSecretDataValid and string-parsing helpers in CTLog utils
  • Import strings package
  • Implement IsSecretDataValid() to scan config lines for backend_spec containing expected address
internal/controller/ctlog/utils/ctlog_config.go
Introduce comprehensive e2e integration tests for CTLog recovery
  • Simulate missing or invalid config by deleting secret
  • Trigger reconciliation via CR annotation
  • Assert operator recreates secret with correct Trillian address
  • Verify CTLog deployment, pod readiness, Ready status, and TreeID preservation
test/e2e/ctlog_recovery_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@petrpinkas petrpinkas marked this pull request as ready for review October 31, 2025 21:15
@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Oct 31, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Weak config validation

Description: Secret validation parses the config using simple substring checks on "backend_spec" and
expected address, which could be bypassed by crafted content or formatting differences,
potentially allowing acceptance of an invalid configuration.
ctlog_config.go [215-236]

Referred Code
func IsSecretDataValid(secretData map[string][]byte, expectedTrillianAddr string) bool {
	if secretData == nil {
		return false
	}

	configData, ok := secretData[ConfigKey]
	if !ok {
		return false
	}

	configString := string(configData)
	if len(configString) == 0 {
		return false
	}

	for _, line := range strings.Split(configString, "\n") {
		if strings.Contains(line, "backend_spec") && strings.Contains(line, expectedTrillianAddr) {
			return true
		}
	}



 ... (clipped 1 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Limited auditing: New controller logic performs secret validation and recreation but only emits basic events
and logs without structured, comprehensive audit details of actions taken and outcomes.

Referred Code
// Validate existing secret before attempting recreation
if instance.Status.ServerConfigRef != nil && instance.Status.ServerConfigRef.Name != "" {
	secret, err := kubernetes.GetSecret(i.Client, instance.Namespace, instance.Status.ServerConfigRef.Name)

	if err != nil {
		if apierrors.IsNotFound(err) {
			i.Logger.Info("Server config secret is missing, will recreate",
				"secret", instance.Status.ServerConfigRef.Name)
			i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigMissing",
				fmt.Sprintf("Config secret is missing, will recreate: %s", instance.Status.ServerConfigRef.Name))
		} else {
			i.Logger.Error(err, "Error accessing server config secret, will attempt to recreate",
				"secret", instance.Status.ServerConfigRef.Name)
			i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigError",
				fmt.Sprintf("Error accessing config secret, will recreate: %s", instance.Status.ServerConfigRef.Name))
		}
	} else {
		// Secret exists and is accessible - validate it
		expectedTrillianAddr := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)
		if ctlogUtils.IsSecretDataValid(secret.Data, expectedTrillianAddr) {
			return i.Continue() // nothing to do


 ... (clipped 9 lines)
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Fragile parsing: Secret validation relies on simple substring matching of 'backend_spec' which
may miss edge cases or alternative formatting and could yield false negatives/positives.

Referred Code
func IsSecretDataValid(secretData map[string][]byte, expectedTrillianAddr string) bool {
	if secretData == nil {
		return false
	}

	configData, ok := secretData[ConfigKey]
	if !ok {
		return false
	}

	configString := string(configData)
	if len(configString) == 0 {
		return false
	}

	for _, line := range strings.Split(configString, "\n") {
		if strings.Contains(line, "backend_spec") && strings.Contains(line, expectedTrillianAddr) {
			return true
		}
	}



 ... (clipped 1 lines)
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured logs: The new log and event messages are unstructured strings and include secret names, which
may be sensitive identifiers and harder to audit programmatically.

Referred Code
if err != nil {
	if apierrors.IsNotFound(err) {
		i.Logger.Info("Server config secret is missing, will recreate",
			"secret", instance.Status.ServerConfigRef.Name)
		i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigMissing",
			fmt.Sprintf("Config secret is missing, will recreate: %s", instance.Status.ServerConfigRef.Name))
	} else {
		i.Logger.Error(err, "Error accessing server config secret, will attempt to recreate",
			"secret", instance.Status.ServerConfigRef.Name)
		i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigError",
			fmt.Sprintf("Error accessing config secret, will recreate: %s", instance.Status.ServerConfigRef.Name))
	}
} else {
	// Secret exists and is accessible - validate it
	expectedTrillianAddr := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)
	if ctlogUtils.IsSecretDataValid(secret.Data, expectedTrillianAddr) {
		return i.Continue() // nothing to do
	}
	// Secret is invalid, will recreate
	i.Logger.Info("Server config secret is invalid, will recreate",
		"secret", secret.Name,


 ... (clipped 5 lines)
  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The IsSecretDataValid helper relies on naive string matching of the protobuf text output, which can be brittle—consider using a TextUnmarshaler or protobuf parser to robustly extract and validate the backend_spec field.
  • CanHandle always returning true forces reconciliation even when nothing has changed; refining its logic (e.g., comparing observed generation or secret checksums) could reduce unnecessary reconcile loops.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The IsSecretDataValid helper relies on naive string matching of the protobuf text output, which can be brittle—consider using a TextUnmarshaler or protobuf parser to robustly extract and validate the backend_spec field.
- CanHandle always returning true forces reconciliation even when nothing has changed; refining its logic (e.g., comparing observed generation or secret checksums) could reduce unnecessary reconcile loops.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Oct 31, 2025

PR Code Suggestions ✨

Latest suggestions up to 018dfb5

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against nil Trillian port
Suggestion Impact:The commit added a guard clause that checks if instance.Spec.Trillian.Port is nil and returns a terminal error before using it to build the Trillian URL, preventing a potential nil dereference.

code diff:

+	switch {
+	case instance.Status.TreeID == nil:
+		return i.Error(ctx, fmt.Errorf("%s: %v", i.Name(), ctlogUtils.ErrTreeNotSpecified), instance)
+	case instance.Status.PrivateKeyRef == nil:
+		return i.Error(ctx, fmt.Errorf("%s: %v", i.Name(), ctlogUtils.ErrPrivateKeyNotSpecified), instance)
+	case instance.Spec.Trillian.Port == nil:
+		return i.Error(ctx, reconcile.TerminalError(fmt.Errorf("%s: %v", i.Name(), ctlogUtils.ErrTrillianPortNotSpecified)), instance)
+	case instance.Spec.Trillian.Address == "":
+		instance.Spec.Trillian.Address = fmt.Sprintf("%s.%s.svc", trillian.LogserverDeploymentName, instance.Namespace)
+	}
+
+	trillianUrl := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)
+

Add a nil check for instance.Spec.Trillian.Port before dereferencing it to
prevent a potential panic, returning an error and setting a failure condition if
it is nil.

internal/controller/ctlog/actions/server_config.go [120]

+if instance.Spec.Trillian.Port == nil {
+	return i.Error(ctx, fmt.Errorf("trillian port not specified"), instance,
+		metav1.Condition{
+			Type:               ConfigCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             constants.Failure,
+			Message:            "Trillian port must be specified",
+			ObservedGeneration: instance.Generation,
+		})
+}
 expectedTrillianAddr := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a potential nil pointer dereference on instance.Spec.Trillian.Port that would crash the controller, and the proposed fix is robust and idiomatic for a Kubernetes controller.

High
Incremental [*]
Strengthen config secret validation

Strengthen the IsSecretDataValid function by adding checks for LogConfigs,
LogId, RootsPemFile, and BackendOverrides to prevent accepting incomplete or
inconsistent configurations.

internal/controller/ctlog/utils/ctlog_config.go [219-249]

 func IsSecretDataValid(secretData map[string][]byte, expectedTrillianAddr string) bool {
 	if secretData == nil {
 		return false
 	}
-
 	configData, ok := secretData[ConfigKey]
 	if !ok || len(configData) == 0 {
 		return false
 	}
 
-	// Parse the protobuf text format configuration
 	var multiConfig configpb.LogMultiConfig
 	if err := prototext.Unmarshal(configData, &multiConfig); err != nil {
-		// Failed to parse - invalid configuration
 		return false
 	}
 
-	// Validate that at least one backend exists
-	if multiConfig.Backends == nil || multiConfig.Backends.Backend == nil || len(multiConfig.Backends.Backend) == 0 {
+	// Validate backends exist and contain expected address
+	if multiConfig.Backends == nil || len(multiConfig.Backends.Backend) == 0 {
+		return false
+	}
+	foundExpected := false
+	for _, backend := range multiConfig.Backends.Backend {
+		if backend.GetBackendSpec() == expectedTrillianAddr {
+			foundExpected = true
+			break
+		}
+	}
+	if !foundExpected {
 		return false
 	}
 
-	// Check if any backend matches the expected Trillian address
-	for _, backend := range multiConfig.Backends.Backend {
-		if backend.BackendSpec == expectedTrillianAddr {
-			return true
+	// Validate at least one log config exists
+	if multiConfig.LogConfigs == nil || len(multiConfig.LogConfigs.Config) == 0 {
+		return false
+	}
+	for _, cfg := range multiConfig.LogConfigs.Config {
+		// Basic sanity checks on per-log config
+		if cfg.GetLogId() == 0 {
+			return false
+		}
+		// Ensure roots file path is present (ctfe requires roots)
+		if cfg.GetRootsPemFile() == "" && cfg.GetRootsPemFilePath() == "" {
+			return false
 		}
 	}
 
-	return false
+	// Ensure overrides (if any) do not contradict expected backend
+	if o := multiConfig.BackendOverrides; o != nil {
+		for _, ov := range o.Override {
+			if ov.GetBackendSpec() != "" && ov.GetBackendSpec() != expectedTrillianAddr {
+				return false
+			}
+		}
+	}
+	return true
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion proposes valid and useful enhancements to the IsSecretDataValid function, making the configuration validation more robust and capable of catching more edge cases that could lead to runtime failures.

Medium
Include secret name in events

Restore the secret name to the CTLogConfigCreated event message to improve
debugging and observability, as was done in the previous version of the code.

internal/controller/ctlog/actions/server_config.go [210]

-i.Recorder.Event(instance, corev1.EventTypeNormal, "CTLogConfigCreated", "Config secret created successfully")
+i.Recorder.Eventf(instance, corev1.EventTypeNormal, "CTLogConfigCreated", "Config secret created successfully: %s", newConfig.Name)
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out that removing the secret name from the event message is a regression in observability. Including the resource name is a best practice for creating useful Kubernetes events.

Low
General
Add robust config sanity checks

Before unmarshalling the config data, trim whitespace and add checks to ensure
the data is not empty or excessively large.

internal/controller/ctlog/utils/ctlog_config.go [231-234]

-if err := prototext.Unmarshal(configData, &multiConfig); err != nil {
-		// Failed to parse - invalid configuration
-		return false
-	}
+// Basic sanity checks to avoid false negatives and resource overuse
+trimmed := bytes.TrimSpace(configData)
+if len(trimmed) == 0 || len(trimmed) > 1_000_000 { // 1MB safety cap
+	return false
+}
+if err := prototext.Unmarshal(trimmed, &multiConfig); err != nil {
+	// Failed to parse - invalid configuration
+	return false
+}
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion adds useful hardening by checking for empty or excessively large config data, but its impact is minor as the existing code already handles most invalid cases and Kubernetes secrets have a size limit.

Low
  • Update

Previous suggestions

✅ Suggestions up to commit 3e1cde2
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate custom server config content
Suggestion Impact:The commit adds validation of the existing server config secret using ctlogUtils.IsSecretDataValid with the expected Trillian address (trillianUrl). If valid, it continues; if invalid, it recreates the secret and records events—implementing the suggested validation intent.

code diff:

+	// Validate prerequisites and normalize Trillian address before validation
 	switch {
 	case instance.Status.TreeID == nil:
 		return i.Error(ctx, fmt.Errorf("%s: %v", i.Name(), ctlogUtils.ErrTreeNotSpecified), instance)
@@ -142,6 +112,36 @@
 	}
 
 	trillianUrl := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)
+
+	// Validate existing secret before attempting recreation
+	if instance.Status.ServerConfigRef != nil && instance.Status.ServerConfigRef.Name != "" {
+		secret, err := kubernetes.GetSecret(i.Client, instance.Namespace, instance.Status.ServerConfigRef.Name)
+
+		if err != nil {
+			if apierrors.IsNotFound(err) {
+				i.Logger.Info("Server config secret is missing, will recreate",
+					"secret", instance.Status.ServerConfigRef.Name)
+				i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigMissing",
+					"Config secret is missing, will recreate")
+			} else {
+				i.Logger.Error(err, "Error accessing server config secret, will attempt to recreate",
+					"secret", instance.Status.ServerConfigRef.Name)
+				i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigError",
+					"Error accessing config secret, will recreate")
+			}
+		} else {
+			// Secret exists and is accessible - validate it
+			if ctlogUtils.IsSecretDataValid(secret.Data, trillianUrl) {
+				return i.Continue() // nothing to do
+			}
+			// Secret is invalid, will recreate
+			i.Logger.Info("Server config secret is invalid, will recreate",
+				"secret", secret.Name,
+				"reason", "Trillian configuration mismatch")
+			i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigInvalid",
+				"Config secret has invalid configuration, will recreate")
+		}
+	}

Add validation for the Trillian address within the custom server config secret
using the ctlogUtils.IsSecretDataValid function to prevent misconfigurations.

internal/controller/ctlog/actions/server_config.go [66-100]

 	if instance.Spec.ServerConfigRef != nil {
 		// Validate that the custom secret is accessible
 		secret, err := kubernetes.GetSecret(i.Client, instance.Namespace, instance.Spec.ServerConfigRef.Name)
 		if err != nil {
 			return i.Error(ctx, fmt.Errorf("error accessing custom server config secret: %w", err), instance,
 				metav1.Condition{
 					Type:               ConfigCondition,
 					Status:             metav1.ConditionFalse,
 					Reason:             constants.Failure,
 					Message:            fmt.Sprintf("Error accessing custom server config secret: %s", instance.Spec.ServerConfigRef.Name),
 					ObservedGeneration: instance.Generation,
 				})
 		}
-		if secret.Data == nil || secret.Data[ctlogUtils.ConfigKey] == nil {
+
+		expectedTrillianAddr := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)
+		if !ctlogUtils.IsSecretDataValid(secret.Data, expectedTrillianAddr) {
 			return i.Error(ctx, fmt.Errorf("custom server config secret is invalid"), instance,
 				metav1.Condition{
 					Type:               ConfigCondition,
 					Status:             metav1.ConditionFalse,
 					Reason:             constants.Failure,
-					Message:            fmt.Sprintf("Custom server config secret is missing '%s' key: %s", ctlogUtils.ConfigKey, instance.Spec.ServerConfigRef.Name),
+					Message:            fmt.Sprintf("Custom server config secret has invalid Trillian address. Expected: %s", expectedTrillianAddr),
 					ObservedGeneration: instance.Generation,
 				})
 		}
 
 		instance.Status.ServerConfigRef = instance.Spec.ServerConfigRef
 		i.Recorder.Event(instance, corev1.EventTypeNormal, "CTLogConfigUpdated", "CTLog config updated")
 		meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{
 			Type:               ConfigCondition,
 			Status:             metav1.ConditionTrue,
 			Reason:             constants.Ready,
 			Message:            "Using custom server config",
 			ObservedGeneration: instance.Generation,
 		})
 		return i.StatusUpdate(ctx, instance)
 	}
Suggestion importance[1-10]: 8

__

Why: This suggestion addresses a potential bug where a custom config with an invalid Trillian address would be accepted, leading to runtime failures that are not reflected in the operator's status.

Medium
Use protobuf unmarshaling for validation
Suggestion Impact:The commit replaced string-based parsing with prototext.Unmarshal into a protobuf config (LogMultiConfig), validated presence of backends, and compared BackendSpec to the expected address, aligning with the suggestion’s intent.

code diff:

@@ -218,17 +222,25 @@
 	}
 
 	configData, ok := secretData[ConfigKey]
-	if !ok {
-		return false
-	}
-
-	configString := string(configData)
-	if len(configString) == 0 {
-		return false
-	}
-
-	for _, line := range strings.Split(configString, "\n") {
-		if strings.Contains(line, "backend_spec") && strings.Contains(line, expectedTrillianAddr) {
+	if !ok || len(configData) == 0 {
+		return false
+	}
+
+	// Parse the protobuf text format configuration
+	var multiConfig configpb.LogMultiConfig
+	if err := prototext.Unmarshal(configData, &multiConfig); err != nil {
+		// Failed to parse - invalid configuration
+		return false
+	}
+
+	// Validate that at least one backend exists
+	if multiConfig.Backends == nil || multiConfig.Backends.Backend == nil || len(multiConfig.Backends.Backend) == 0 {
+		return false
+	}
+
+	// Check if any backend matches the expected Trillian address
+	for _, backend := range multiConfig.Backends.Backend {
+		if backend.BackendSpec == expectedTrillianAddr {
 			return true
 		}

Refactor IsSecretDataValid to unmarshal the prototext configuration into a
struct for validation, instead of using fragile string matching.

internal/controller/ctlog/utils/ctlog_config.go [215-237]

 func IsSecretDataValid(secretData map[string][]byte, expectedTrillianAddr string) bool {
 	if secretData == nil {
 		return false
 	}
 
 	configData, ok := secretData[ConfigKey]
-	if !ok {
+	if !ok || len(configData) == 0 {
 		return false
 	}
 
-	configString := string(configData)
-	if len(configString) == 0 {
+	var cfg configpb.LogConfig
+	if err := prototext.Unmarshal(configData, &cfg); err != nil {
 		return false
 	}
 
-	for _, line := range strings.Split(configString, "\n") {
-		if strings.Contains(line, "backend_spec") && strings.Contains(line, expectedTrillianAddr) {
-			return true
-		}
+	if cfg.GetLogBackend() == nil {
+		return false
 	}
 
-	return false
+	return cfg.GetLogBackend().GetBackendSpec() == expectedTrillianAddr
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that string-based parsing of prototext is brittle and proposes a more robust solution using prototext.Unmarshal, which improves code quality and reliability.

Low

@osmman osmman added the bug Something isn't working label Nov 6, 2025
return true
case instance.Spec.ServerConfigRef != nil:
return !equality.Semantic.DeepEqual(instance.Spec.ServerConfigRef, instance.Status.ServerConfigRef)
case c.ObservedGeneration != instance.Generation:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If custom ServerConfigRef is unchanged, generation changes are ignored and periodic validation doesn't run.

default:
return instance.Generation != c.ObservedGeneration
// Always run Handle() to validate the secret: exists and is valid
return true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When default is true than some of prev validation are not needed.

if apierrors.IsNotFound(err) {
i.Logger.Info("Server config secret is missing, will recreate",
"secret", instance.Status.ServerConfigRef.Name)
i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigMissing",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide to all logs and events the name of the secret map. The name is generated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is on multiple places so please fix it for all of them.


trillianUrl := fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port)

// Validate existing secret before attempting recreation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract whole block of validation existing secret to new function which will return error based on actual state. Then in Handle function simple call it if no error found return i.Continue() other way use infrormation from the error to create event and log message.

"secret", instance.Status.ServerConfigRef.Name)
i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigMissing",
"Config secret is missing, will recreate")
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for k8s api error other than not found will be better to return failure reconcilation. The reason is that there could be a lot of different api error like access rejection which creating a new object will not solve the problem and it cause other issues.

"reason", "Trillian configuration mismatch")
i.Recorder.Event(instance, corev1.EventTypeWarning, "CTLogConfigInvalid",
"Config secret has invalid Trillian configuration, will recreate")
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why checking list if root certificate is not handled by IsSecretDataValid and checking only size of list is insufficient it will need to compare that exactly correct certificates are used from status.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to solve that by adding annotation which will contains information based on which data it has been generated. We have it for example in Fulcio's generate_cert action and we are using them to check if data has been generated from same spec.

For that you can use CRDs generation or compare exactly spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants