Skip to content

Enable TLS support for PostgreSQL (#3831)#484

Merged
fghanmi merged 2 commits intomainfrom
cherry-pick-pg-tls
Dec 10, 2025
Merged

Enable TLS support for PostgreSQL (#3831)#484
fghanmi merged 2 commits intomainfrom
cherry-pick-pg-tls

Conversation

@fghanmi
Copy link
Copy Markdown
Member

@fghanmi fghanmi commented Dec 4, 2025

This PR adds TLS support for PostgreSQL connections in the Trillian server/signer.

The key changes include:

Added new flags:

  • postgresql_tls_ca: Path to the CA certificate file for PostgreSQL TLS connection.
  • postgresql_verify_full: Enable full TLS verification for PostgreSQL (sslmode=verify-full). If false, only sslmode=verify-ca is used. If no TLS configuration is provided, the connection defaults to non-TLS, ensuring backward compatibility.

Tracking issue: google#3830


Checklist

This PR adds TLS support for PostgreSQL connections in the Trillian server/signer. The key changes include:
Added new flags:

postgresql_tls_ca: Path to the CA certificate file for PostgreSQL TLS connection.
postgresql_verify_full: Enable full TLS verification for PostgreSQL (sslmode=verify-full). If false, only sslmode=verify-ca is used.
If no TLS configuration is provided, the connection defaults to non-TLS, ensuring backward compatibility.

Tracking issue: google#3830

---------

Signed-off-by: Firas Ghanmi <fghanmi@redhat.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Dec 4, 2025

Reviewer's Guide

Implements configurable TLS support for PostgreSQL connections by adding new flags, a URI-rewriting helper that injects TLS parameters, wiring it into the PostgreSQL provider initialization, and covering the behavior with unit tests and a CHANGELOG entry.

Sequence diagram for PostgreSQL connection initialization with TLS URI rewriting

sequenceDiagram
    participant TrillianServer
    participant postgresqlProvider
    participant getPostgreSQLDatabaseLocked
    participant BuildPostgresTLSURI
    participant os
    participant url
    participant OpenDB
    participant PostgreSQLServer

    TrillianServer->>postgresqlProvider: New()
    postgresqlProvider->>getPostgreSQLDatabaseLocked: getPostgreSQLDatabaseLocked()
    getPostgreSQLDatabaseLocked->>getPostgreSQLDatabaseLocked: check cached postgresqlDB, postgresqlErr
    getPostgreSQLDatabaseLocked->>BuildPostgresTLSURI: BuildPostgresTLSURI(postgresql_uri)

    alt TLS CA not configured
        BuildPostgresTLSURI-->>getPostgreSQLDatabaseLocked: return original uri
    else TLS CA configured
        BuildPostgresTLSURI->>os: Stat(postgresql_tls_ca)
        alt CA file error
            os-->>BuildPostgresTLSURI: error
            BuildPostgresTLSURI-->>getPostgreSQLDatabaseLocked: error
            getPostgreSQLDatabaseLocked-->>postgresqlProvider: postgresqlErr
            postgresqlProvider-->>TrillianServer: error
        else CA file ok
            os-->>BuildPostgresTLSURI: ok
            BuildPostgresTLSURI->>url: Parse(uri)
            alt invalid uri
                url-->>BuildPostgresTLSURI: error
                BuildPostgresTLSURI-->>getPostgreSQLDatabaseLocked: error
                getPostgreSQLDatabaseLocked-->>postgresqlProvider: postgresqlErr
                postgresqlProvider-->>TrillianServer: error
            else valid uri
                url-->>BuildPostgresTLSURI: parsed url
                BuildPostgresTLSURI->>BuildPostgresTLSURI: set sslrootcert, sslmode
                BuildPostgresTLSURI-->>getPostgreSQLDatabaseLocked: modified uri
            end
        end
    end

    getPostgreSQLDatabaseLocked->>OpenDB: OpenDB(uri)
    OpenDB->>PostgreSQLServer: establish connection (TLS or non TLS)
    PostgreSQLServer-->>OpenDB: connection established
    OpenDB-->>getPostgreSQLDatabaseLocked: *pgxpool.Pool
    getPostgreSQLDatabaseLocked-->>postgresqlProvider: postgresqlDB
    postgresqlProvider-->>TrillianServer: ready
Loading

Class diagram for PostgreSQL provider and TLS URI helper

classDiagram
    class postgresqlProvider {
        -db *pgxpool.Pool
        +Close() error
    }

    class Flags {
        <<global>>
        +postgreSQLURI *string
        +postgresqlTLSCA *string
        +postgresqlVerifyFull *bool
    }

    class TLSURIHelper {
        +BuildPostgresTLSURI(uri string) (string, error)
    }

    class PostgreSQLStorageState {
        <<package_level>>
        +postgresqlMu sync.Mutex
        +postgresqlErr error
        +postgresqlDB *pgxpool.Pool
    }

    postgresqlProvider --> PostgreSQLStorageState : uses
    postgresqlProvider --> Flags : reads
    postgresqlProvider --> TLSURIHelper : calls
    TLSURIHelper --> Flags : reads
    TLSURIHelper --> PostgreSQLStorageState : sets postgresqlErr
Loading

File-Level Changes

Change Details Files
Add TLS configuration flags and URI transformation in PostgreSQL storage provider.
  • Introduce postgresql_tls_ca and postgresql_verify_full flags to control TLS behavior for PostgreSQL connections.
  • Add BuildPostgresTLSURI helper that validates the CA file, parses the base URI, and injects sslrootcert and sslmode query params according to the flags.
  • Wire BuildPostgresTLSURI into getPostgreSQLDatabaseLocked so that OpenDB always receives a URI with the correct TLS settings or fails early on misconfiguration.
  • Propagate configuration/parse errors through the existing postgresqlErr global for consistent error handling.
storage/postgresql/provider.go
Add unit tests for PostgreSQL TLS URI building behavior.
  • Test that URIs are left unchanged when no TLS CA is configured.
  • Test that an error is returned when the configured CA file path does not exist.
  • Test verify-ca behavior: sslrootcert is set to the CA path and sslmode defaults to verify-ca when verify_full is false.
  • Test verify-full behavior: sslrootcert is set and sslmode is set to verify-full when verify_full is true.
storage/postgresql/provider_test.go
Document new PostgreSQL TLS functionality.
  • Add CHANGELOG entry describing PostgreSQL TLS support and the new postgresql_tls_ca and postgresql_verify_full flags.
CHANGELOG.md

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

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 4, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive information exposure

Description: The function assigns detailed parse/IO errors (including the full PostgreSQL URI) to a
package-level variable postgresqlErr, which is reused across provider operations,
potentially exposing sensitive connection information (e.g., credentials or hosts) to
global state and logs where postgresqlErr is later surfaced.
provider.go [119-126]

Referred Code
if _, err := os.Stat(*postgresqlTLSCA); err != nil {
	postgresqlErr = fmt.Errorf("postgresql CA file error: %w", err)
	return "", postgresqlErr
}
u, err := url.Parse(uri)
if err != nil {
	postgresqlErr = fmt.Errorf("invalid postgresql URI %q: %w", uri, err)
	return "", postgresqlErr
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

Learn more about managing compliance generic rules or creating your own custom rules

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

Learn more about managing compliance generic rules or creating your own custom rules

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: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

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

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

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

Status:
No audit logs: The new TLS configuration and connection-building logic adds critical behavior without
emitting any audit logs of configuration usage or outcomes.

Referred Code
	uri := *postgreSQLURI
	var err error
	uri, err = BuildPostgresTLSURI(uri)
	if err != nil {
		postgresqlErr = err
		return nil, err
	}
	db, err := OpenDB(uri)
	if err != nil {
		postgresqlErr = err
		return nil, err
	}
	postgresqlDB, postgresqlErr = db, nil
	return db, nil
}

func (s *postgresqlProvider) LogStorage() storage.LogStorage {
	return NewLogStorage(s.db, s.mf)
}

func (s *postgresqlProvider) AdminStorage() storage.AdminStorage {


 ... (clipped 35 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

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

Status:
Error propagation: BuildPostgresTLSURI sets the package-global postgresqlErr and returns it, which may
conflate initialization errors and hinder retries or independent error contexts.

Referred Code
if _, err := os.Stat(*postgresqlTLSCA); err != nil {
	postgresqlErr = fmt.Errorf("postgresql CA file error: %w", err)
	return "", postgresqlErr
}
u, err := url.Parse(uri)
if err != nil {
	postgresqlErr = fmt.Errorf("invalid postgresql URI %q: %w", uri, err)
	return "", postgresqlErr
}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Copy link
Copy Markdown

@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:

  • Consider making BuildPostgresTLSURI unexported (e.g. buildPostgresTLSURI) since it’s only used within this package, which keeps the external API surface smaller while still allowing it to be tested.
  • In BuildPostgresTLSURI, you’re mutating the package-level postgresqlErr inside a helper; it would be cleaner and less surprising to just return fmt.Errorf(...) from this function and only set postgresqlErr in getPostgreSQLDatabaseLocked.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making `BuildPostgresTLSURI` unexported (e.g. `buildPostgresTLSURI`) since it’s only used within this package, which keeps the external API surface smaller while still allowing it to be tested.
- In `BuildPostgresTLSURI`, you’re mutating the package-level `postgresqlErr` inside a helper; it would be cleaner and less surprising to just return `fmt.Errorf(...)` from this function and only set `postgresqlErr` in `getPostgreSQLDatabaseLocked`.

## Individual Comments

### Comment 1
<location> `storage/postgresql/provider.go:115-124` </location>
<code_context>
+
+// BuildPostgresTLSURI modifies the given PostgreSQL URI to include TLS parameters based on flags.
+// It returns the modified URI and any error encountered.
+func BuildPostgresTLSURI(uri string) (string, error) {
+	if *postgresqlTLSCA == "" {
+		return uri, nil
+	}
+	if _, err := os.Stat(*postgresqlTLSCA); err != nil {
+		postgresqlErr = fmt.Errorf("postgresql CA file error: %w", err)
+		return "", postgresqlErr
+	}
+	u, err := url.Parse(uri)
+	if err != nil {
+		postgresqlErr = fmt.Errorf("invalid postgresql URI %q: %w", uri, err)
+		return "", postgresqlErr
+	}
+	q := u.Query()
+	q.Set("sslrootcert", *postgresqlTLSCA)
+	if *postgresqlVerifyFull {
+		q.Set("sslmode", "verify-full")
+	} else {
+		if q.Get("sslmode") == "" {
+			q.Set("sslmode", "verify-ca")
+		}
+	}
+	u.RawQuery = q.Encode()
+	return u.String(), nil
+}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid mutating the global `postgresqlErr` inside this helper to reduce hidden side effects and possible races.

This helper both returns an error and mutates the package-level `postgresqlErr`. Because `BuildPostgresTLSURI` is exported and not clearly tied to the `getPostgreSQLDatabaseLocked` mutex, it could race on `postgresqlErr` or clobber an existing error when called from elsewhere. Prefer making this function side‑effect‑free (returning only `(string, error)`) and have the caller update `postgresqlErr` under the appropriate lock if needed.
</issue_to_address>

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.

Comment on lines +115 to +124
func BuildPostgresTLSURI(uri string) (string, error) {
if *postgresqlTLSCA == "" {
return uri, nil
}
if _, err := os.Stat(*postgresqlTLSCA); err != nil {
postgresqlErr = fmt.Errorf("postgresql CA file error: %w", err)
return "", postgresqlErr
}
u, err := url.Parse(uri)
if err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid mutating the global postgresqlErr inside this helper to reduce hidden side effects and possible races.

This helper both returns an error and mutates the package-level postgresqlErr. Because BuildPostgresTLSURI is exported and not clearly tied to the getPostgreSQLDatabaseLocked mutex, it could race on postgresqlErr or clobber an existing error when called from elsewhere. Prefer making this function side‑effect‑free (returning only (string, error)) and have the caller update postgresqlErr under the appropriate lock if needed.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 4, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Enforce secure default TLS mode

When a CA certificate is provided and postgresql_verify_full is false,
unconditionally set sslmode to verify-ca to override any potentially insecure
sslmode specified in the connection URI.

storage/postgresql/provider.go [130-136]

 	if *postgresqlVerifyFull {
 		q.Set("sslmode", "verify-full")
 	} else {
-		if q.Get("sslmode") == "" {
-			q.Set("sslmode", "verify-ca")
-		}
+		q.Set("sslmode", "verify-ca")
 	}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a security weakness where providing a CA certificate does not guarantee a secure sslmode, and proposes a fix that enforces a secure default, preventing insecure configurations.

Medium
High-level
Use connection URI for TLS

Instead of adding new flags like --postgresql_tls_ca for TLS, configure TLS
options directly within the --postgresql_uri connection string. This is a more
standard, scalable, and flexible approach.

Examples:

storage/postgresql/provider.go [31-139]
	postgreSQLURI        = flag.String("postgresql_uri", "postgresql:///defaultdb?host=localhost&user=test", "Connection URI for PostgreSQL database")
	postgresqlTLSCA      = flag.String("postgresql_tls_ca", "", "Path to the CA certificate file for PostgreSQL TLS connection ")
	postgresqlVerifyFull = flag.Bool("postgresql_verify_full", false, "Enable full TLS verification for PostgreSQL (sslmode=verify-full). If false, only sslmode=verify-ca is used.")

	postgresqlMu              sync.Mutex
	postgresqlErr             error
	postgresqlDB              *pgxpool.Pool
	postgresqlStorageInstance *postgresqlProvider
)


 ... (clipped 99 lines)

Solution Walkthrough:

Before:

// storage/postgresql/provider.go
var (
  postgreSQLURI        = flag.String("postgresql_uri", ...)
  postgresqlTLSCA      = flag.String("postgresql_tls_ca", ...)
  postgresqlVerifyFull = flag.Bool("postgresql_verify_full", ...)
)

func getPostgreSQLDatabaseLocked() (*pgxpool.Pool, error) {
  uri := *postgreSQLURI
  uri, err = BuildPostgresTLSURI(uri)
  if err != nil {
    return nil, err
  }
  db, err := OpenDB(uri)
  // ...
  return db, nil
}

func BuildPostgresTLSURI(uri string) (string, error) {
  // ... logic to parse URI and add TLS params from flags ...
}

After:

// storage/postgresql/provider.go
var (
  // Only the URI flag is needed.
  postgreSQLURI = flag.String("postgresql_uri", ...)
)

func getPostgreSQLDatabaseLocked() (*pgxpool.Pool, error) {
  // The BuildPostgresTLSURI function is no longer needed.
  // Users configure TLS directly in the URI, e.g.:
  // --postgresql_uri="...&sslmode=verify-full&sslrootcert=/path/to/ca.pem"
  db, err := OpenDB(*postgreSQLURI)
  if err != nil {
    postgresqlErr = err
    return nil, err
  }
  postgresqlDB = db
  return db, nil
}
Suggestion importance[1-10]: 7

__

Why: The suggestion offers a valid and superior design alternative by leveraging the standard connection URI for configuration, which enhances scalability and flexibility while simplifying the codebase by removing special-purpose flags and logic.

Medium
General
Remove global state modification from function

Refactor BuildPostgresTLSURI to remove the side effect of modifying the global
postgresqlErr variable. The function should only return an error, and the caller
should be responsible for managing the global state.

storage/postgresql/provider.go [119-127]

 	if _, err := os.Stat(*postgresqlTLSCA); err != nil {
-		postgresqlErr = fmt.Errorf("postgresql CA file error: %w", err)
-		return "", postgresqlErr
+		return "", fmt.Errorf("postgresql CA file error: %w", err)
 	}
 	u, err := url.Parse(uri)
 	if err != nil {
-		postgresqlErr = fmt.Errorf("invalid postgresql URI %q: %w", uri, err)
-		return "", postgresqlErr
+		return "", fmt.Errorf("invalid postgresql URI %q: %w", uri, err)
 	}
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that BuildPostgresTLSURI unnecessarily modifies a global variable, which is a code smell. Removing this side effect improves code quality by making the function pure and easier to test and reason about.

Low
  • Update

Signed-off-by: Firas Ghanmi <fghanmi@redhat.com>
@fghanmi fghanmi merged commit b4bf785 into main Dec 10, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants