Skip to content

feat(etcd): support TLS via OpenSSL#49

Merged
bzp2010 merged 21 commits into
mainfrom
feat/etcd-tls-openssl
Apr 20, 2026
Merged

feat(etcd): support TLS via OpenSSL#49
bzp2010 merged 21 commits into
mainfrom
feat/etcd-tls-openssl

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

@AlinsRan AlinsRan commented Apr 20, 2026

Why

The original implementation used etcd-client's tls feature, which is backed by rustls. This has several drawbacks:

  1. Only supports TLSv1.2 and TLSv1.3, blocking connections to servers that require older TLS versions or cipher suites outside the TLS 1.2+ subset.
  2. Introduces ring/aws-lc-rs as additional cryptographic dependencies, adding compilation overhead and FFI risk.
  3. Does not support asynchronous encryption/decryption, making TLS hardware offloading impossible.

The project already links against OpenSSL (used by reqwest via native-tls and by axum-server), so reusing it for etcd TLS avoids pulling in a second cryptographic stack.

What changed

  • Switched etcd-client from features = ["tls"] to features = ["tls-openssl"].
  • Added EtcdTlsCertConfig enum with two variants:
    • Strings { ca, cert, key } — inline PEM-encoded content (e.g. from environment variables or secrets managers).
    • Files { ca_file, cert_file, key_file } — paths to PEM files on disk.
  • EtcdTlsConfig holds an optional cert: Option<EtcdTlsCertConfig> and insecure_skip_verify (default false). It deserializes from a flat config block; mixing inline strings and file paths in the same block is rejected at parse time.
  • Convenience methods ca_pem(), cert_pem(), key_pem() on both types abstract over the two sources so call sites are source-agnostic.
  • TLS is enabled automatically when any etcd host URL uses the https:// scheme.
  • Mixed http:///https:// host lists and hosts without a valid scheme are rejected with a clear typed error (EtcdConfigError).
  • cert and key (or cert_file and key_file) must be provided together for mTLS; specifying only one is an error.
  • insecure_skip_verify: true disables all certificate validation (hostname, CA); intended for development/testing only.
  • Custom Debug implementations redact all inline PEM content and password fields to prevent sensitive material leaking into logs. File paths are shown as-is.
  • Validation logic is extracted into validate_connect_config() returning a typed EtcdConfigError, making error handling precise and matchable.

Configuration

Two forms are supported and are mutually exclusive within the same tls block.

File paths:

deployment:
  etcd:
    host: ["https://etcd.example.com:2379"]
    tls:
      ca_file: /path/to/ca.pem       # optional: CA cert to verify server
      cert_file: /path/to/cert.pem   # optional: client cert for mTLS (must pair with key_file)
      key_file: /path/to/key.pem     # optional: client key  for mTLS (must pair with cert_file)
      insecure_skip_verify: false    # optional: skip TLS verification (dev/test only)

Inline PEM strings (suitable for container environments where secrets are injected as environment variables):

deployment:
  etcd:
    host: ["https://etcd.example.com:2379"]
    tls:
      ca: |
        -----BEGIN CERTIFICATE-----
        ...
        -----END CERTIFICATE-----
      cert: |
        -----BEGIN CERTIFICATE-----
        ...
        -----END CERTIFICATE-----
      key: |
        -----BEGIN PRIVATE KEY-----
        ...
        -----END PRIVATE KEY-----

tls is optional. When omitted, the system's default CA store is used for server certificate verification.

@AlinsRan AlinsRan requested a review from bzp2010 as a code owner April 20, 2026 00:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add optional PEM-based TLS config for etcd (ca_file, cert_file, key_file, insecure_skip_verify), enable tls-openssl feature for etcd-client, detect/reject mixed http/https hosts, apply OpenSSL TLS (with mTLS validation) when connecting, and add unit tests and config.yaml entries.

Changes

Cohort / File(s) Summary
Dependency Configuration
Cargo.toml
Changed etcd-client from a version-only entry to an explicit table: etcd-client = { version = "0.18.0", features = ["tls-openssl"] }, enabling OpenSSL TLS support.
TLS Config & Connection Logic
src/config/etcd.rs
Added public EtcdTlsConfig with ca_file, cert_file, key_file, and insecure_skip_verify; extended Config with tls: Option<EtcdTlsConfig>; detect/reject mixed http:///https:// hosts; build OpenSslClientConfig, enforce mTLS pair presence, call with_openssl_tls when HTTPS is used; adjusted connect_client return type and readiness handling.
Config File
config.yaml
Added deployment.etcd.tls block with ca_file, cert_file, key_file, and insecure_skip_verify: false.
Tests
src/config/etcd.rs (#[cfg(test)] module)
Added unit tests for defaults, scheme detection and mixed-scheme rejection, partial mTLS validation errors, read_pem, and serde deserialization when TLS is present/absent.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant Provider as EtcdConfigProvider
    participant OpenSSL as OpenSslClientConfig
    participant Etcd as etcd-server

    Caller->>Provider: connect_client(config)
    Provider->>Provider: parse hosts, detect "https://" vs "http://"
    alt HTTPS detected
        Provider->>OpenSSL: build TLS config (load ca_file, cert_file, key_file, set insecure_skip_verify)
        OpenSSL-->>Provider: OpenSSL TLS config
        Provider->>Etcd: connect using with_openssl_tls(...)
    else HTTP only
        Provider->>Etcd: connect without TLS
    end
    Etcd-->>Provider: respond (status)
    Provider-->>Caller: return client or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning No E2E tests found exercising actual etcd connection with TLS; only unit tests for config parsing and validation logic exist. Add integration tests using containerized etcd with TLS to verify successful connections, certificate validation, and error handling.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding TLS support for etcd using OpenSSL, which aligns with the primary objective and all modified files.
Security Check ✅ Passed The TLS implementation demonstrates correct security practices with proper insecure_skip_verify semantics, safe PEM file handling, enforced mTLS pairing, and no credential exposure in logs or error responses.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/etcd-tls-openssl

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/etcd.rs`:
- Around line 23-28: Add triple-slash doc comments for the public EtcdTlsConfig
struct and its fields (EtcdTlsConfig, ca_pem, cert_pem, key_pem) describing each
field's purpose and expected format (e.g., PEM-encoded CA, client cert, client
key), and add a /// doc comment on the public Config::tls field explaining that
it holds optional TLS configuration for connecting to etcd; ensure comments
follow repo style and cover that fields are optional and when they should be
provided.
- Around line 101-113: The code currently enables TLS when any host in
config.host is "https://" and silently ignores partial mTLS settings; update the
logic in the etcd client setup (the block that computes use_tls and builds
OpenSslClientConfig) so that TLS is enabled only if all hosts use the "https://"
scheme (fail with an error if host list contains mixed "http://" and
"https://"), and validate config.tls so that if either cert_pem or key_pem is
present without the other you return/fail immediately with a clear configuration
error; continue to apply ca_pem when present and only call
opts.with_openssl_tls(tls_cfg) after these validations succeed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e10613f6-cb8f-430f-a5ee-a599b4da565e

📥 Commits

Reviewing files that changed from the base of the PR and between 6be90c7 and 7b2c171.

📒 Files selected for processing (2)
  • Cargo.toml
  • src/config/etcd.rs

Comment thread src/config/etcd.rs
Comment thread src/config/etcd.rs Outdated
- Regenerate Cargo.lock to include new OpenSSL TLS dependencies
  introduced by switching etcd-client to features = ["tls-openssl"]
- Add 9 unit tests covering EtcdTlsConfig defaults, deserialization,
  and TLS host detection logic
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/config/etcd.rs (2)

23-28: ⚠️ Potential issue | 🟡 Minor

Add /// docs for the new public TLS config items.

Line 23 and Line 37 introduce public API without Rust doc comments. Please document EtcdTlsConfig, its fields, and Config::tls.

Suggested patch
 #[derive(Clone, Debug, Default, Deserialize)]
+/// TLS material for connecting to etcd over HTTPS.
 pub struct EtcdTlsConfig {
+    /// Optional PEM-encoded CA certificate used to validate the etcd server certificate.
     pub ca_pem: Option<String>,
+    /// Optional PEM-encoded client certificate for mTLS authentication.
     pub cert_pem: Option<String>,
+    /// Optional PEM-encoded private key matching `cert_pem`.
     pub key_pem: Option<String>,
 }
@@
 pub struct Config {
@@
+    /// Optional TLS settings applied when etcd endpoints use `https://`.
     pub tls: Option<EtcdTlsConfig>,
 }

As per coding guidelines: Use /// for doc comments on public items in Rust.

Also applies to: 37-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 23 - 28, Public API items lack Rust doc
comments: add /// documentation for the EtcdTlsConfig struct and each public
field (EtcdTlsConfig, ca_pem, cert_pem, key_pem) and for Config::tls; describe
purpose, expected formats/encodings (e.g., PEM), optionality, and any security
or usage notes (e.g., when each field is required), keeping the docs concise and
using /// on the struct and each field as per Rust guidelines.

93-113: ⚠️ Potential issue | 🟠 Major

Harden TLS setup: reject mixed host schemes and incomplete mTLS keypairs.

Line 101 enables TLS if any endpoint is HTTPS, and Lines 108-110 ignore partial cert_pem/key_pem. Both should fail fast to avoid ambiguous/broken runtime behavior.

Suggested patch
-    async fn connect_client(config: &Config) -> Result<etcd_client::Client, etcd_client::Error> {
+    async fn connect_client(config: &Config) -> Result<etcd_client::Client> {
         let mut opts = etcd_client::ConnectOptions::default()
             .with_connect_timeout(Duration::from_secs(config.timeout as u64));
@@
-        let use_tls = config.host.iter().any(|h| h.starts_with("https://"));
+        let has_https = config.host.iter().any(|h| h.starts_with("https://"));
+        let has_http = config.host.iter().any(|h| h.starts_with("http://"));
+        if has_http && has_https {
+            return Err(anyhow!(
+                "etcd hosts must use a single scheme (all http:// or all https://)"
+            ));
+        }
+        let use_tls = has_https;
         if use_tls {
             let mut tls_cfg = etcd_client::OpenSslClientConfig::default();
             if let Some(t) = &config.tls {
                 if let Some(ca_pem) = &t.ca_pem {
                     tls_cfg = tls_cfg.ca_cert_pem(ca_pem.as_bytes());
                 }
-                if let (Some(cert_pem), Some(key_pem)) = (&t.cert_pem, &t.key_pem) {
-                    tls_cfg = tls_cfg.client_cert_pem_and_key(cert_pem.as_bytes(), key_pem.as_bytes());
-                }
+                match (&t.cert_pem, &t.key_pem) {
+                    (Some(cert_pem), Some(key_pem)) => {
+                        tls_cfg =
+                            tls_cfg.client_cert_pem_and_key(cert_pem.as_bytes(), key_pem.as_bytes());
+                    }
+                    (None, None) => {}
+                    _ => return Err(anyhow!("both tls.cert_pem and tls.key_pem must be set together")),
+                }
             }
             opts = opts.with_openssl_tls(tls_cfg);
         }
@@
-        client.status().await.map(|_| Ok(client))?
+        client.status().await?;
+        Ok(client)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 93 - 113, The connect_client function
currently enables TLS if any host starts with "https://" and silently ignores
partial mTLS keypairs; change it to validate up-front: compute whether all hosts
are HTTPS or all are non-HTTPS and return an error if schemes are mixed (do not
proceed when config.host contains both "http://" and "https://"), and when
config.tls is present require both cert_pem and key_pem together (return an
error if only one is set). Update the use_tls decision and the TLS block in
connect_client to perform these checks and return a descriptive Err from
connect_client instead of silently continuing; reference connect_client,
config.host, use_tls, config.tls, cert_pem and key_pem to locate the code to
change.
🧹 Nitpick comments (1)
src/config/etcd.rs (1)

423-518: Use repository-preferred assertion macros in tests.

The new tests use standard assert_eq!/assert!; this repo prefers pretty_assertions::assert_eq and assert_matches::assert_matches for test diagnostics.

Suggested patch (pattern)
 #[cfg(test)]
 mod tests {
     use super::*;
+    use assert_matches::assert_matches;
+    use pretty_assertions::assert_eq;
@@
-        assert!(tls.ca_pem.is_none());
+        assert_matches!(tls.ca_pem, None);
@@
-        assert_eq!(cfg.host, vec!["http://127.0.0.1:2379"]);
+        assert_eq!(cfg.host, vec!["http://127.0.0.1:2379"]);

As per coding guidelines: Use pretty_assertions::assert_eq and assert_matches::assert_matches for better test output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 423 - 518, Replace standard
assert_eq!/assert! in the tests (e.g. test_etcd_tls_config_default,
test_config_default_no_tls, test_use_tls_detected_from_https_host,
test_use_tls_not_detected_for_http_host,
test_use_tls_mixed_hosts_any_https_triggers_tls,
test_etcd_tls_config_deserialization,
test_etcd_tls_config_partial_deserialization,
test_config_deserialization_with_tls, test_config_deserialization_without_tls)
with the repository-preferred macros: use pretty_assertions::assert_eq for
equality checks on EtcdTlsConfig and Config values and use
assert_matches::assert_matches where pattern matching assertions are needed; add
the necessary use imports at the top of the test module so
EtcdTlsConfig::default(), Config::default(), serde_json::from_str(...)
assertions use assert_eq and assert_matches consistently across all tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/config/etcd.rs`:
- Around line 23-28: Public API items lack Rust doc comments: add ///
documentation for the EtcdTlsConfig struct and each public field (EtcdTlsConfig,
ca_pem, cert_pem, key_pem) and for Config::tls; describe purpose, expected
formats/encodings (e.g., PEM), optionality, and any security or usage notes
(e.g., when each field is required), keeping the docs concise and using /// on
the struct and each field as per Rust guidelines.
- Around line 93-113: The connect_client function currently enables TLS if any
host starts with "https://" and silently ignores partial mTLS keypairs; change
it to validate up-front: compute whether all hosts are HTTPS or all are
non-HTTPS and return an error if schemes are mixed (do not proceed when
config.host contains both "http://" and "https://"), and when config.tls is
present require both cert_pem and key_pem together (return an error if only one
is set). Update the use_tls decision and the TLS block in connect_client to
perform these checks and return a descriptive Err from connect_client instead of
silently continuing; reference connect_client, config.host, use_tls, config.tls,
cert_pem and key_pem to locate the code to change.

---

Nitpick comments:
In `@src/config/etcd.rs`:
- Around line 423-518: Replace standard assert_eq!/assert! in the tests (e.g.
test_etcd_tls_config_default, test_config_default_no_tls,
test_use_tls_detected_from_https_host, test_use_tls_not_detected_for_http_host,
test_use_tls_mixed_hosts_any_https_triggers_tls,
test_etcd_tls_config_deserialization,
test_etcd_tls_config_partial_deserialization,
test_config_deserialization_with_tls, test_config_deserialization_without_tls)
with the repository-preferred macros: use pretty_assertions::assert_eq for
equality checks on EtcdTlsConfig and Config values and use
assert_matches::assert_matches where pattern matching assertions are needed; add
the necessary use imports at the top of the test module so
EtcdTlsConfig::default(), Config::default(), serde_json::from_str(...)
assertions use assert_eq and assert_matches consistently across all tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7e07efa-717a-430a-90c4-5b6728332e51

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2c171 and 6dbc75c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • src/config/etcd.rs

- Add doc comments to EtcdTlsConfig fields and Config::tls
- Reject mixed http/https host schemes with a clear error message
- Reject partial mTLS config (cert_pem without key_pem, or vice versa)
- Update connect_client return type to anyhow::Result for consistency
- Add unit tests for the new validation paths
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/config/etcd.rs (1)

466-498: Prefer behavior tests over predicate-reimplementation tests.

The host-scheme tests in this block re-implement starts_with checks and don’t validate connect_client behavior. Consider replacing them with outcome-based tests only (success/failure paths), which are less brittle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 466 - 498, The tests re-check host schemes
with starts_with instead of exercising the real logic; replace the three
predicate tests (test_tls_detected_from_https_host,
test_tls_not_detected_for_http_host, test_mixed_http_https_hosts_detected) with
outcome-driven tests that construct Config instances (using Config::default()
and Config { host: vec![...] }) and call the connect_client function (or the
public API that uses it) to assert the actual behavior: successful connection
for all-http hosts, successful connection for all-https hosts (or TLS-enabled
path), and a rejected/error result for mixed http+https hosts; assert on the
returned Result/Err or error kind rather than re-implementing starts_with checks
on cfg.host.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/etcd.rs`:
- Around line 447-604: The tests in this module should use the
repository-standard assertion macros: add imports for
pretty_assertions::assert_eq and assert_matches::assert_matches at the top of
the tests module, then replace plain assert_eq! calls in tests like
test_etcd_tls_config_default, test_config_default_no_tls,
test_etcd_tls_config_deserialization,
test_etcd_tls_config_partial_deserialization,
test_config_deserialization_with_tls, and
test_config_deserialization_without_tls with pretty_assertions::assert_eq; also
replace the match-based error checks in async tests
test_connect_client_rejects_mixed_schemes,
test_connect_client_rejects_partial_mtls_cert_only, and
test_connect_client_rejects_partial_mtls_key_only with assert_matches!(Err(e), e
if e.to_string().contains("...")) (or equivalent assert_matches patterns) so the
tests use assert_matches! for error assertions and pretty_assertions::assert_eq
for equality.

---

Nitpick comments:
In `@src/config/etcd.rs`:
- Around line 466-498: The tests re-check host schemes with starts_with instead
of exercising the real logic; replace the three predicate tests
(test_tls_detected_from_https_host, test_tls_not_detected_for_http_host,
test_mixed_http_https_hosts_detected) with outcome-driven tests that construct
Config instances (using Config::default() and Config { host: vec![...] }) and
call the connect_client function (or the public API that uses it) to assert the
actual behavior: successful connection for all-http hosts, successful connection
for all-https hosts (or TLS-enabled path), and a rejected/error result for mixed
http+https hosts; assert on the returned Result/Err or error kind rather than
re-implementing starts_with checks on cfg.host.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32973bdd-99df-4711-ab9b-3284716d6de5

📥 Commits

Reviewing files that changed from the base of the PR and between 6dbc75c and 9638eb3.

📒 Files selected for processing (1)
  • src/config/etcd.rs

Comment thread src/config/etcd.rs
Replace standard assert_eq! and manual match blocks with project-standard
assertion macros per coding guidelines.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/config/etcd.rs (1)

469-501: These scheme-detection tests are tautological; prefer behavior-level coverage.

These tests re-implement starts_with checks instead of asserting connect_client outcomes. Consider consolidating into parameterized cases that verify accept/reject behavior directly.

As per coding guidelines: Use #[test] for synchronous unit tests, #[tokio::test] for async tests, and #[rstest] for parameterized tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 469 - 501, The tests currently probe host
scheme detection by re-implementing starts_with checks on Config.host; instead,
update them to exercise the actual behavior by calling connect_client (or the
function that validates/creates the client) and asserting whether the call
succeeds or returns an error for each scenario. Replace the three tautological
tests with a parameterized #[rstest] (or multiple #[tokio::test]/#[test] as
appropriate) that supplies hosts like "https://...", "http://...", and a mixed
list and then asserts connect_client returns Ok for all-https, Ok or Err per
your intended policy for all-http, and Err for mixed; reference the Config
struct and connect_client function in the test so the tests validate behavior
rather than re-checking starts_with.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/etcd.rs`:
- Around line 108-114: The current validation only checks for mixed http/https
but allows endpoints like "127.0.0.1:2379"; update the validation so every entry
in config.host is required to start with either "http://" or "https://",
returning an error if any host does not match those prefixes before deciding TLS
mode; modify the logic around config.host, has_http and has_https to first
assert all hosts have an explicit scheme (fail fast on non-HTTP(S) endpoints),
then proceed with the existing mixed-scheme check.

---

Nitpick comments:
In `@src/config/etcd.rs`:
- Around line 469-501: The tests currently probe host scheme detection by
re-implementing starts_with checks on Config.host; instead, update them to
exercise the actual behavior by calling connect_client (or the function that
validates/creates the client) and asserting whether the call succeeds or returns
an error for each scenario. Replace the three tautological tests with a
parameterized #[rstest] (or multiple #[tokio::test]/#[test] as appropriate) that
supplies hosts like "https://...", "http://...", and a mixed list and then
asserts connect_client returns Ok for all-https, Ok or Err per your intended
policy for all-http, and Err for mixed; reference the Config struct and
connect_client function in the test so the tests validate behavior rather than
re-checking starts_with.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a1fbe2b-67c9-46ff-8cc0-6f14f612c016

📥 Commits

Reviewing files that changed from the base of the PR and between 9638eb3 and f40d024.

📒 Files selected for processing (1)
  • src/config/etcd.rs

Comment thread src/config/etcd.rs Outdated
…erify

- Remove inline PEM string fields (ca_pem, cert_pem, key_pem); keep only
  file-based fields (ca_file, cert_file, key_file)
- Replace resolve_pem helper with simpler read_pem (file path only)
- Add insecure_skip_verify field (default false) to skip TLS certificate
  validation; useful for development environments with IP/hostname mismatches
- Update config.yaml to use insecure_skip_verify: true for current test env
- Update tests to match simplified struct and helper
…t, reject invalid schemes

- Move cert/key pair presence check before read_pem calls so the error
  message is always 'cert and key must be set together' regardless of
  whether the files exist (fixes two flaky CI test failures)
- Implement custom Debug for EtcdTlsConfig and Config that redacts
  key_file and password fields to prevent sensitive material leaking
  into logs
- Reject etcd host entries that lack a http:// or https:// scheme with
  a clear error message
- Add test_connect_client_rejects_missing_scheme for the new validation
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/config/etcd.rs (1)

130-136: ⚠️ Potential issue | 🟠 Major

Reject endpoints without explicit http:// or https:// scheme.

The validation only rejects explicit mixed schemes, but a host like 127.0.0.1:2379 (no scheme) bypasses this check. When combined with an https:// endpoint, this creates an inconsistent TLS state.

Suggested fix
-        let has_https = config.host.iter().any(|h| h.starts_with("https://"));
-        let has_http = config.host.iter().any(|h| h.starts_with("http://"));
+        let mut has_https = false;
+        let mut has_http = false;
+        for h in &config.host {
+            if h.starts_with("https://") {
+                has_https = true;
+            } else if h.starts_with("http://") {
+                has_http = true;
+            } else {
+                return Err(anyhow!(
+                    "etcd host must start with http:// or https://: {}",
+                    h
+                ));
+            }
+        }
         if has_http && has_https {
             return Err(anyhow!(
                 "etcd hosts must use a single scheme (all http:// or all https://)"
             ));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 130 - 136, The host-scheme validation in
etcd.rs currently only checks for mixed http/https via has_http and has_https
but allows entries without any scheme (e.g., "127.0.0.1:2379"), which can create
inconsistent TLS state; update the validation around config.host to require
every host string to start with either "http://" or "https://" and return an
error if any host lacks a scheme, and continue to reject mixed schemes as before
(i.e., compute a flag like has_scheme per-host or check for
!(starts_with("http://") || starts_with("https://")) and surface a clear anyhow!
error mentioning config.host and the offending entry), referencing the existing
variables has_http / has_https and config.host to locate and replace the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/config/etcd.rs`:
- Around line 130-136: The host-scheme validation in etcd.rs currently only
checks for mixed http/https via has_http and has_https but allows entries
without any scheme (e.g., "127.0.0.1:2379"), which can create inconsistent TLS
state; update the validation around config.host to require every host string to
start with either "http://" or "https://" and return an error if any host lacks
a scheme, and continue to reject mixed schemes as before (i.e., compute a flag
like has_scheme per-host or check for !(starts_with("http://") ||
starts_with("https://")) and surface a clear anyhow! error mentioning
config.host and the offending entry), referencing the existing variables
has_http / has_https and config.host to locate and replace the logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22b3b8f7-863b-4f98-906d-cfcd350ad5a9

📥 Commits

Reviewing files that changed from the base of the PR and between f40d024 and feb9b03.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • config.yaml
  • src/config/etcd.rs
✅ Files skipped from review due to trivial changes (1)
  • config.yaml

Add ca_pem, cert_pem, key_pem fields to EtcdTlsConfig so that TLS
credentials can be supplied as inline strings (e.g. from env vars)
rather than only from files on disk.  File-based fields take precedence
when both are supplied for the same credential.

This is useful for environments where certificates are injected via
environment variables rather than mounted as files.

- Update connect_client to resolve each credential from file or inline PEM
- Update validation to cover cross-source cert/key pair mismatches
- Add tests for inline PEM deserialization and partial pair rejection
Comment thread src/config/etcd.rs Outdated
Comment thread src/config/etcd.rs Outdated
Comment thread src/config/etcd.rs Outdated
AlinsRan and others added 4 commits April 20, 2026 16:33
…g redaction

- Add EtcdConfigError enum (thiserror) with MixedSchemes, MissingScheme,
  PartialMtlsKeypair variants for precise, matchable error types
- Extract host scheme and mTLS keypair validation into validate_connect_config()
  returning Result<bool, EtcdConfigError> instead of inline anyhow errors
- Fix EtcdTlsConfig Debug: key_file path is no longer redacted; only PEM
  content strings (ca_pem, cert_pem, key_pem) are redacted
- Add 5 direct unit tests for validate_connect_config covering each error
  variant and the happy paths (http/https)
Remove ca_pem, cert_pem, key_pem fields from EtcdTlsConfig and all
associated code paths, tests, and documentation references. TLS
certificate material is now exclusively supplied via file paths
(ca_file, cert_file, key_file).
…ile-based certs

Add EtcdTlsCertConfig enum with Strings and Files variants to unify
cert/key material from two sources. Methods ca_pem()/cert_pem()/key_pem()
abstract file I/O vs. inline strings behind a single interface.

EtcdTlsConfig deserializes from a flat config structure via a custom
Deserialize impl (intermediate EtcdTlsConfigRaw struct) that rejects
mixed inline+file configs at parse time. Convenience methods on
EtcdTlsConfig delegate to the inner cert config so callers avoid
Option boilerplate. Deref<Target=Option<EtcdTlsCertConfig>> is also
implemented for direct pattern matching on the cert variant.
Comment thread src/config/etcd.rs Outdated
insecure_skip_verify: bool,
}

impl<'de> Deserialize<'de> for EtcdTlsConfig {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

https://serde.rs/attr-flatten.html

Why not use serde's native flattening? A custom parser would require writing a lot of boilerplate code. In most cases, this is unnecessary.

Comment thread src/config/etcd.rs
})
}

async fn connect_client(config: &Config) -> Result<etcd_client::Client, etcd_client::Error> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return to the original pattern.

The validate_connect_config should not be included within connect_client, because connect_client is an internal-only function with a fairly clear semantic: "establish a connection."

Configuration errors can be checked for and thrown during EtcdConfigProvider::new -> Result<()>; there is no need to enter the connect_client function.

Comment thread src/config/etcd.rs Outdated
Comment thread src/config/etcd.rs Outdated
Comment thread src/config/etcd.rs
Comment on lines +220 to +221
#[error("etcd hosts must use a single scheme (all http:// or all https://)")]
MixedSchemes,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this a requirement for etcd-client?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, client.status() is not a requirement of etcd-client. It was added as an early connection health-check, but calling the Maintenance service during connect_client is unnecessary and caused 'unknown service etcdserverpb.Maintenance' errors when the server does not expose that gRPC service. Removed in this PR.

AlinsRan and others added 6 commits April 20, 2026 17:51
Co-authored-by: Zeping Bai <zeping@api7.ai>
Co-authored-by: Zeping Bai <zeping@api7.ai>
- Replace EtcdTlsConfigRaw + custom Deserialize with PemFiles/PemStrings
  helper structs using #[serde(deny_unknown_fields)] and #[serde(untagged)]
  on EtcdTlsCertConfig, eliminating manual deserialization boilerplate
- Change EtcdTlsCertConfig from struct variants to tuple variants; add
  Default impl (-> Files with all None) so EtcdTlsConfig.cert is non-Optional
- Remove EtcdTlsConfig wrapper pem methods; callers use Deref to EtcdTlsCertConfig
- Change validate_connect_config return type to Result<(), EtcdConfigError>
  and move the call to EtcdConfigProvider::new() so config errors fail fast
  without entering retry logic
- Remove client.status().await? from connect_client
- Update tests accordingly; remove redundant connect_client validation tests
…nfigRaw serde

Keep validate_connect_config in new() and client.status() removal,
but restore the original struct-variant enum and custom Deserialize
approach to avoid the pub(crate) interface visibility issue with
untagged enum tuple variants.
…tagged

Use #[serde(untagged, deny_unknown_fields)] on EtcdTlsCertConfig and
#[serde(flatten)] on EtcdTlsConfig.cert to eliminate the EtcdTlsConfigRaw
intermediate struct and the hand-written Deserialize implementation.

The untagged enum tries Files first, so file-path fields take precedence
when deserializing. Mixed inputs (e.g. ca_file + cert) are rejected
because deny_unknown_fields causes both variants to fail.

EtcdTlsConfig.cert is now EtcdTlsCertConfig (non-Optional) with a Default
of Files { all None }; pem accessors and validate_connect_config are
updated accordingly.
Copy link
Copy Markdown
Collaborator

@bzp2010 bzp2010 left a comment

Choose a reason for hiding this comment

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

I approve this change in principle. I will address the remaining issues in next pull request.

Comment thread src/config/etcd.rs
Comment thread src/config/etcd.rs Outdated
Comment thread src/config/etcd.rs Outdated
@bzp2010 bzp2010 merged commit fc1bc22 into main Apr 20, 2026
10 checks passed
@bzp2010 bzp2010 deleted the feat/etcd-tls-openssl branch April 20, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants