Skip to content

feat(qdrant): add TLS support via cert-manager - #2685

Merged
Arsolitt (Arsolitt) merged 9 commits into
mainfrom
feat/tls-qdrant
May 29, 2026
Merged

feat(qdrant): add TLS support via cert-manager#2685
Arsolitt (Arsolitt) merged 9 commits into
mainfrom
feat/tls-qdrant

Conversation

@Arsolitt

@Arsolitt Arsolitt (Arsolitt) commented May 19, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds TLS support to the Qdrant managed service via cert-manager.

  • Introduces a nullable tls.enabled field with tri-state semantics: when unset, defaults to the value of external (auto-on for externally published services, off for cluster-internal). Explicit tls.enabled always wins.
  • Renders a self-contained cert-manager chain in the tenant namespace: self-signed Issuer, CA Certificate, CA Issuer, leaf Certificate.
  • SAN policy covers the three DNS forms per service (bare, .ns.svc, .ns.svc.<cluster-domain>) plus the external hostname when external: true.
  • Wires the leaf cert into the Qdrant HelmRelease so both the REST and gRPC endpoints use the chart-managed CA chain.

Verified end-to-end on a sandbox cluster: cert chain reaches Ready, TLS handshake against the tenant service succeeds with the chart CA bundle.

Release note

feat(qdrant): add TLS support via cert-manager (tri-state tls.enabled)

Summary by CodeRabbit

  • New Features

    • TLS config for Qdrant with tri-state behavior (inherit from external, or explicitly enable/disable); optional cert-manager provisioning and automatic TLS secret mounting.
  • Documentation

    • README and chart values/schema updated to document TLS parameters, defaults, and precedence.
  • Tests

    • Expanded Helm tests covering TLS rendering, certificates (SANs/ECDSA), reload annotations, and volume/secret injection.
  • Chores

    • Added a Helm test target to the Makefile.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bf53767e-d87e-4996-9b31-6438bfcc7d56

📥 Commits

Reviewing files that changed from the base of the PR and between 7544cdb and 87ad834.

📒 Files selected for processing (11)
  • api/apps/v1alpha1/qdrant/types.go
  • api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go
  • packages/apps/qdrant/Makefile
  • packages/apps/qdrant/README.md
  • packages/apps/qdrant/templates/_helpers.tpl
  • packages/apps/qdrant/templates/certmanager.yaml
  • packages/apps/qdrant/templates/qdrant.yaml
  • packages/apps/qdrant/tests/certmanager_test.yaml
  • packages/apps/qdrant/tests/qdrant_test.yaml
  • packages/apps/qdrant/values.schema.json
  • packages/apps/qdrant/values.yaml
💤 Files with no reviewable changes (9)
  • packages/apps/qdrant/values.yaml
  • packages/apps/qdrant/templates/_helpers.tpl
  • packages/apps/qdrant/Makefile
  • packages/apps/qdrant/README.md
  • packages/apps/qdrant/templates/qdrant.yaml
  • packages/apps/qdrant/values.schema.json
  • packages/apps/qdrant/tests/certmanager_test.yaml
  • packages/apps/qdrant/templates/certmanager.yaml
  • packages/apps/qdrant/tests/qdrant_test.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go

📝 Walkthrough

Walkthrough

Adds explicit TLS configuration and helper to compute effective TLS, cert-manager templates for certificate generation, Helm wiring to enable/mount TLS, generated deepcopy support, values/schema/docs, Makefile test target, and extensive template/unit tests.

Changes

Qdrant TLS Configuration and Certificate Management

Layer / File(s) Summary
API Type Contracts
api/apps/v1alpha1/qdrant/types.go
ConfigSpec gains optional Tls field; new TLS struct with nullable Enabled bool allows explicit TLS override or inheritance from external when unset.
Generated Deepcopy Methods
api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go
Autogenerated DeepCopyInto and DeepCopy methods updated for ConfigSpec.Tls field and added for TLS type, safely handling nullable pointer.
System Application Schema
packages/system/qdrant-rd/cozyrds/qdrant.yaml
Updates embedded openAPISchema to include tls object with enabled boolean and adds spec.tls to keysOrder.
Helm Values Schema and Documentation
packages/apps/qdrant/values.schema.json, packages/apps/qdrant/values.yaml, packages/apps/qdrant/README.md
JSON schema defines tls object with optional enabled boolean; values file and README document inheritance/override semantics relative to external access flag.
Helm Helper: tls.enabled
packages/apps/qdrant/templates/_helpers.tpl
Adds qdrant.tls.enabled helper that returns .Values.tls.enabled when set, otherwise falls back to .Values.external.
Cert-Manager Template Implementation
packages/apps/qdrant/templates/certmanager.yaml
Conditionally generates cert-manager resources (self-signed Issuer, CA certificate, CA-backed Issuer, and leaf Certificate) when TLS enabled; computes cluster domain and renders DNS SANs including per-replica and optional external hostnames.
Cert-Manager Template Tests
packages/apps/qdrant/tests/certmanager_test.yaml
Test suite validates resource generation in disabled/enabled states, tri-state precedence with external, naming/secretName conventions, internal/external SAN generation, per-replica SANs, and ECDSA-specific assertions.
Qdrant Template Integration
packages/apps/qdrant/templates/qdrant.yaml
Wires TLS enablement into service.enable_tls and cluster.p2p.enable_tls; conditionally renders TLS file paths, adds secret reloader annotation, and mounts TLS Secret into the pod.
Qdrant Template Test Suite
packages/apps/qdrant/tests/qdrant_test.yaml
Validates TLS propagation across disabled/enabled states and tri-state precedence; asserts certificate paths, TLS secret volume/mounts, service.type LoadBalancer behavior, standalone P2P TLS, and reloader annotation presence.
Build Infrastructure
packages/apps/qdrant/Makefile
Adds test target to run helm unittest ..

Sequence Diagram(s)

sequenceDiagram
  participant Values as "Helm Values"
  participant Templating as "Helm Templating Engine"
  participant K8s as "Kubernetes API"
  participant CertManager as "cert-manager"
  participant Qdrant as "Qdrant Pod"
  Values->>Templating: render templates (qdrant.tls.enabled, certmanager.yaml, qdrant.yaml)
  Templating->>K8s: apply Issuer/Certificate resources when tls enabled
  CertManager->>K8s: create Secret with issued cert and key and ca
  K8s->>Qdrant: mount Secret as volume and set reloader annotation
  Qdrant->>Qdrant: load cert key and ca from /qdrant/tls and enable TLS
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 A little rabbit hops to say,
TLS now guards the Qdrant way.
Cert-manager crafts the keys,
Templates mount them with great ease,
Secure connections hop and stay.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(qdrant): add TLS support via cert-manager' accurately and specifically summarizes the main change—adding TLS functionality to Qdrant via cert-manager integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tls-qdrant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature labels May 19, 2026
@Arsolitt
Arsolitt (Arsolitt) marked this pull request as ready for review May 19, 2026 13:03
@dosubot dosubot Bot added the lifecycle/active Indicates that an issue or PR is actively being worked on by a contributor label May 19, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the Qdrant managed service by integrating cert-manager for automated TLS certificate management. It provides a robust configuration mechanism that defaults to secure-by-default behavior based on external access requirements, while allowing explicit overrides. The changes include updates to the API types, Helm templates, and schema definitions, accompanied by thorough unit testing to ensure correct rendering of certificates and service configurations.

Highlights

  • TLS Support: Added TLS support for Qdrant using cert-manager, enabling secure communication for both REST and gRPC endpoints.
  • Tri-state Configuration: Introduced a tls.enabled field that defaults to the external setting, allowing for flexible cluster-internal or external TLS enablement.
  • Cert-manager Integration: Implemented a self-contained certificate chain (Issuer, CA, and leaf Certificate) with comprehensive SAN coverage for service DNS forms.
  • Testing: Added comprehensive unit tests for both the certificate generation logic and the HelmRelease configuration.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/zz_generated.*.go (1)
    • api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces TLS support for the Qdrant application by adding a tls configuration field to the API and Helm values. The changes include a new certmanager.yaml template for certificate generation, updates to the qdrant.yaml HelmRelease to configure TLS and volume mounts, and the addition of comprehensive unit tests. Feedback focuses on improving template maintainability by extracting TLS enablement logic into a common Helm helper and simplifying the volume mount logic by removing redundant empty list assignments.

Comment thread packages/apps/qdrant/templates/qdrant.yaml Outdated
Comment thread packages/apps/qdrant/templates/qdrant.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/apps/qdrant/tests/qdrant_test.yaml (1)

160-247: ⚡ Quick win

Add explicit tls.enabled: null inheritance test cases.

The suite validates unset/true/false, but the nullable mode (null) is not covered. Adding these assertions will lock in tri-state behavior and prevent regressions.

Suggested test additions
+  # (e) external: false, tls.enabled: null -> TLS OFF (null inherits)
+  - it: "(e) TLS is OFF when external is false and tls.enabled is null"
+    release:
+      name: test-qdrant
+      namespace: tenant-test
+    set:
+      external: false
+      tls:
+        enabled: null
+    asserts:
+      - equal:
+          path: spec.values.qdrant.config.service.enable_tls
+          value: false
+      - equal:
+          path: spec.values.qdrant.config.cluster.p2p.enable_tls
+          value: false
+
+  # (f) external: true, tls.enabled: null -> TLS ON (null inherits)
+  - it: "(f) TLS is ON when external is true and tls.enabled is null"
+    release:
+      name: test-qdrant
+      namespace: tenant-test
+    set:
+      external: true
+      tls:
+        enabled: null
+    asserts:
+      - equal:
+          path: spec.values.qdrant.config.service.enable_tls
+          value: true
+      - equal:
+          path: spec.values.qdrant.config.cluster.p2p.enable_tls
+          value: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/qdrant/tests/qdrant_test.yaml` around lines 160 - 247, Add two
canonical tri-state tests that assert nullable inheritance when tls.enabled is
explicitly null: one where set: { external: false, tls: { enabled: null } }
should result in spec.values.qdrant.config.service.enable_tls and
spec.values.qdrant.config.cluster.p2p.enable_tls being false and
spec.values.qdrant.additionalVolumes/additionalVolumeMounts being empty, and
another where set: { external: true, tls: { enabled: null } } should result in
those enable_tls fields being true and spec.values.qdrant.additionalVolumes
containing the tls secret (secretName: test-qdrant-tls); mirror the style and
assertions used in the existing cases "(a)"–"(d)" to ensure tri-state null
behavior is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/apps/qdrant/templates/qdrant.yaml`:
- Around line 1-7: The template treats tls.enabled present-but-null as an
explicit value; change the assignment logic for $tlsEnabled so null falls back
to .Values.external (and then to false) instead of being used as-is. Replace the
hasKey/index branch with a single safe default expression that uses index
$tlsMap "enabled" but falls back to .Values.external (and then false) when
nil—for example use Helm's default function: set $tlsEnabled = default
(.Values.external | default false) (index $tlsMap "enabled") so null behaves
like unset.

---

Nitpick comments:
In `@packages/apps/qdrant/tests/qdrant_test.yaml`:
- Around line 160-247: Add two canonical tri-state tests that assert nullable
inheritance when tls.enabled is explicitly null: one where set: { external:
false, tls: { enabled: null } } should result in
spec.values.qdrant.config.service.enable_tls and
spec.values.qdrant.config.cluster.p2p.enable_tls being false and
spec.values.qdrant.additionalVolumes/additionalVolumeMounts being empty, and
another where set: { external: true, tls: { enabled: null } } should result in
those enable_tls fields being true and spec.values.qdrant.additionalVolumes
containing the tls secret (secretName: test-qdrant-tls); mirror the style and
assertions used in the existing cases "(a)"–"(d)" to ensure tri-state null
behavior is covered.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18212de2-150e-43bd-9deb-b98a92dbbbe6

📥 Commits

Reviewing files that changed from the base of the PR and between c5e9287 and bc25a7d.

📒 Files selected for processing (11)
  • api/apps/v1alpha1/qdrant/types.go
  • api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go
  • packages/apps/qdrant/Makefile
  • packages/apps/qdrant/README.md
  • packages/apps/qdrant/templates/certmanager.yaml
  • packages/apps/qdrant/templates/qdrant.yaml
  • packages/apps/qdrant/tests/certmanager_test.yaml
  • packages/apps/qdrant/tests/qdrant_test.yaml
  • packages/apps/qdrant/values.schema.json
  • packages/apps/qdrant/values.yaml
  • packages/system/qdrant-rd/cozyrds/qdrant.yaml

Comment thread packages/apps/qdrant/templates/qdrant.yaml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
packages/apps/qdrant/templates/qdrant.yaml (1)

1-7: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle tls.enabled: null as inherit-from-external, not as explicit override.

This segment has the same tri-state null handling bug as identified in the past review comment. When tls.enabled is explicitly set to null, the hasKey check passes and index assigns nil to $tlsEnabled, causing lines 53 and 56 to render blank YAML (enable_tls: ) instead of valid booleans.

🔧 Proposed fix
 {{- $tlsMap := default (dict) .Values.tls -}}
 {{- $tlsEnabled := false -}}
-{{- if hasKey $tlsMap "enabled" -}}
-  {{- $tlsEnabled = index $tlsMap "enabled" -}}
-{{- else -}}
+{{- $tlsRaw := index $tlsMap "enabled" -}}
+{{- if and (hasKey $tlsMap "enabled") (ne $tlsRaw nil) -}}
+  {{- $tlsEnabled = $tlsRaw -}}
+{{- else -}}
   {{- $tlsEnabled = .Values.external | default false -}}
 {{- end -}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/qdrant/templates/qdrant.yaml` around lines 1 - 7, The current
logic sets $tlsEnabled to nil when .Values.tls.enabled is explicitly null
because it assigns index $tlsMap "enabled" whenever hasKey returns true; update
the conditional for $tlsEnabled so it only uses index $tlsMap "enabled" when
that value is non-nil, otherwise fall back to .Values.external | default false.
Concretely, replace the hasKey branch for $tlsEnabled with a check like: if
hasKey $tlsMap "enabled" and (index $tlsMap "enabled") is not nil then set
$tlsEnabled = index $tlsMap "enabled" else set $tlsEnabled = .Values.external |
default false so enable_tls renders a valid boolean instead of blank when
tls.enabled is null.
packages/apps/qdrant/templates/certmanager.yaml (1)

5-11: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle tls.enabled: null as inherit-from-external, not as explicit override.

The tri-state contract documented in the values schema requires that tls.enabled should inherit from .Values.external when unset or null. However, the current code only checks key presence with hasKey. When tls.enabled is explicitly set to null, it passes the hasKey check, and index assigns nil to $tlsEnabled. This breaks the inheritance semantics and could prevent cert-manager resources from rendering correctly.

🔧 Proposed fix
 {{- $tlsMap := default (dict) .Values.tls -}}
 {{- $tlsEnabled := false -}}
-{{- if hasKey $tlsMap "enabled" -}}
-  {{- $tlsEnabled = index $tlsMap "enabled" -}}
-{{- else -}}
+{{- $tlsRaw := index $tlsMap "enabled" -}}
+{{- if and (hasKey $tlsMap "enabled") (ne $tlsRaw nil) -}}
+  {{- $tlsEnabled = $tlsRaw -}}
+{{- else -}}
   {{- $tlsEnabled = .Values.external | default false -}}
 {{- end -}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/qdrant/templates/certmanager.yaml` around lines 5 - 11, The
code treats the presence of tls.enabled as an override even when it's null;
update the assignment for $tlsEnabled so null inherits from .Values.external by
using Helm's default to coalesce nil: replace the hasKey/index branch with a
single assignment that sets $tlsEnabled = default (.Values.external | default
false) (index $tlsMap "enabled") (or equivalent nested default), so when index
returns nil it falls back to .Values.external (and that in turn defaults to
false).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/apps/qdrant/templates/certmanager.yaml`:
- Around line 5-11: The code treats the presence of tls.enabled as an override
even when it's null; update the assignment for $tlsEnabled so null inherits from
.Values.external by using Helm's default to coalesce nil: replace the
hasKey/index branch with a single assignment that sets $tlsEnabled = default
(.Values.external | default false) (index $tlsMap "enabled") (or equivalent
nested default), so when index returns nil it falls back to .Values.external
(and that in turn defaults to false).

In `@packages/apps/qdrant/templates/qdrant.yaml`:
- Around line 1-7: The current logic sets $tlsEnabled to nil when
.Values.tls.enabled is explicitly null because it assigns index $tlsMap
"enabled" whenever hasKey returns true; update the conditional for $tlsEnabled
so it only uses index $tlsMap "enabled" when that value is non-nil, otherwise
fall back to .Values.external | default false. Concretely, replace the hasKey
branch for $tlsEnabled with a check like: if hasKey $tlsMap "enabled" and (index
$tlsMap "enabled") is not nil then set $tlsEnabled = index $tlsMap "enabled"
else set $tlsEnabled = .Values.external | default false so enable_tls renders a
valid boolean instead of blank when tls.enabled is null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3612a096-8d01-450a-bed7-815c73bd946b

📥 Commits

Reviewing files that changed from the base of the PR and between bc25a7d and 75e3caf.

📒 Files selected for processing (11)
  • api/apps/v1alpha1/qdrant/types.go
  • api/apps/v1alpha1/qdrant/zz_generated.deepcopy.go
  • packages/apps/qdrant/Makefile
  • packages/apps/qdrant/README.md
  • packages/apps/qdrant/templates/certmanager.yaml
  • packages/apps/qdrant/templates/qdrant.yaml
  • packages/apps/qdrant/tests/certmanager_test.yaml
  • packages/apps/qdrant/tests/qdrant_test.yaml
  • packages/apps/qdrant/values.schema.json
  • packages/apps/qdrant/values.yaml
  • packages/system/qdrant-rd/cozyrds/qdrant.yaml
✅ Files skipped from review due to trivial changes (1)
  • packages/apps/qdrant/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/apps/qdrant/values.yaml
  • packages/system/qdrant-rd/cozyrds/qdrant.yaml
  • packages/apps/qdrant/values.schema.json
  • packages/apps/qdrant/tests/certmanager_test.yaml
  • packages/apps/qdrant/tests/qdrant_test.yaml
  • api/apps/v1alpha1/qdrant/types.go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NOT LGTM — TLS scaffold is mostly clean (cert chain is well-shaped, leaf usages cover both server-auth and client-auth for p2p mTLS, tls.enabled tri-state via hasKey is sound), but the SAN list cannot match the short-form pod hostnames Qdrant uses for p2p bootstrap, and there's no restart trigger when cert-manager rotates the leaf — TLS will silently break at the one-year mark.

Business context: Add TLS to the managed Qdrant service for both client REST/gRPC (6333/6334) and inter-node p2p (6335) via a self-contained cert-manager chain, with tls.enabled falling back to external when unset.

Blockers

B1: SAN list does not cover Qdrant's short-form p2p peer hostnames

File: packages/apps/qdrant/templates/certmanager.yaml:61-68

Issue: When replicas > 1, the upstream Qdrant chart starts each pod with --bootstrap '<protocol>://<release>-0.<release>-headless:6335' and --uri '<protocol>://<release>-<N>.<release>-headless:6335' (see packages/system/qdrant/charts/qdrant/templates/configmap.yaml, initialize.sh). These URIs use the short form <release>-N.<release>-headless — no .<ns>.svc.<cluster> suffix. X.509 wildcards match exactly one DNS label at the leftmost position; *.<release>-headless.<ns>.svc.<cluster> cannot match <release>-0.<release>-headless (different label count).

Evidence: configmap.yaml:initialize.sh builds the URI from qdrant.fullname + -headless — no FQDN suffix. The peer TLS client (Qdrant's tonic-based p2p) verifies the cert's SAN list against the literal URI hostname, not the post-resolution FQDN. Cert SAN list at certmanager.yaml:62-68 only covers <release>{,.<ns>.svc{,.<cluster>}}, the bare headless service plus its FQDN forms, and the wildcard *.<release>-headless.<ns>.svc.<cluster> — none of which match <release>-N.<release>-headless.

Impact: Multi-replica + TLS-enabled deployments fail to form a cluster — qdrant-1/qdrant-2/… each attempt the --bootstrap URI against qdrant-0.<release>-headless and the TLS handshake fails with a hostname-mismatch error. Single-replica deployments (the chart default) happen to work because qdrant-0's self-URI is never dialed.

Fix: Add SANs for each pod's short form. The chart already has .Values.replicas, so:

{{- range $i, $_ := until (int .Values.replicas) }}
- {{ printf "%s-%d.%s-headless" $.Release.Name $i $.Release.Name | quote }}
- {{ printf "%s-%d.%s-headless.%s.svc" $.Release.Name $i $.Release.Name $.Release.Namespace | quote }}
- {{ printf "%s-%d.%s-headless.%s.svc.%s" $.Release.Name $i $.Release.Name $.Release.Namespace $clusterDomain | quote }}
{{- end }}

(Scaling up later requires reissuing the cert; alternatively use a multi-label wildcard escape via the SVID/SPIFFE-style URI SAN, but the per-replica approach is simpler.)

B2: No restart trigger on cert-manager rotation — TLS breaks silently at the cert-expiry mark

File: packages/apps/qdrant/templates/qdrant.yaml:57-65 + templates/certmanager.yaml:47-48

Issue: cert-manager rotates the leaf every ~11 months (duration: 8760h, renewBefore: 720h). Qdrant reads tls.cert/tls.key/tls.ca_cert from /qdrant/tls/ at process start and does not watch the file for changes — neither actix-web (REST) nor tonic (gRPC) hot-reloads server certs in Qdrant's current configuration. After rotation:

  1. kubelet syncs the new Secret content into the mounted volume (the on-disk file is fresh).
  2. Qdrant keeps serving with the in-memory copy of the original cert.
  3. At month-12, the original cert hits notAfter. Clients now reject the handshake with certificate has expired.

Nothing in the HelmRelease re-triggers a rolling restart when the Secret content changes — Flux's helm-controller hashes the chart artifact + HelmRelease values, not the contents of side-loaded Secrets.

Evidence: packages/system/qdrant/charts/qdrant/templates/statefulset.yaml carries checksum/config and checksum/secret annotations, but they hash configmap.yaml and secret.yaml (the API-key secret) — not the cert-manager-managed TLS Secret created by templates/certmanager.yaml. No reloader.stakater.com/* annotation is set on the pod template (cozystack ships stakater/reloader at packages/system/reloader/, so the operator is available cluster-wide).

Impact: A successfully-installed TLS-enabled Qdrant instance becomes unreachable to all clients exactly 12 months after install with no warning, until an operator manually restarts the StatefulSet. The renewBefore window helps cert-manager refresh on disk early, but does nothing because Qdrant never re-reads.

Fix: Pass an annotation that the reloader operator picks up:

{{- if $tlsEnabled }}
podAnnotations:
  reloader.stakater.com/auto: "true"
{{- end }}

…or pin it to the TLS secret specifically:

podAnnotations:
  secret.reloader.stakater.com/reload: "{{ .Release.Name }}-tls"

Upstream chart supports podAnnotations in statefulset.yaml (verified). With reloader installed cluster-side, a Secret content change will trigger a rolling restart and the new cert is loaded before the old one expires.

Non-blocking follow-ups

  1. qdrant.yaml:57-69 — the additionalVolumes / additionalVolumeMounts block is unconditionally rewritten (overwriting any value coming from the cozystack-values Secret, including the explicit [] reset in the TLS-disabled branch). Tenants in the standard managed flow can't reach those keys via the ApplicationDefinition surface, so the practical regression is narrow, but the chart-level habit of overwriting upstream extension points instead of appending is worth flagging — if Qdrant's user values ever need an extra mount, this will silently drop it.

  2. certmanager.yaml:28-29: CA privateKey only sets rotationPolicy: Never, leaving algorithm/size at cert-manager defaults (RSA-2048). Leaf cert has no privateKey block at all. Mariadb (#2680) and other sibling PRs pin RSA-4096 for the CA — non-uniform but not a blocker; RSA-2048 is still a sound choice for a tenant-scoped CA.

  3. _helpers.tpl:1-13 — the tri-state implementation uses hasKey $tlsMap "enabled" and relies on values.schema.json rejecting null (since enabled is type: boolean, not ["boolean","null"]). That's correct given the current schema but tightly coupled — a future schema relaxation to null would let null reach the template as the string "<nil>" and silently flip TLS on (since neither "true" nor "false"). Worth either tightening the template (treat anything other than "true"/"false" as the external fallback) or pinning the schema with a comment that null is intentionally rejected.

@lexfrei Aleksei Sviridkin (lexfrei) added area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) and removed area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review labels May 25, 2026
@Arsolitt

Copy link
Copy Markdown
Contributor Author

Addressed your review.

Blockers:

  • B1 (SAN list missing short-form pod hostnames Qdrant uses for p2p bootstrap) — fixed in 8a426487c. Per-replica short-form SANs are now added via range over .Values.replicas in the leaf cert's dnsNames, covering <release>-N.<release>-headless in all three resolution forms (bare, .svc, .svc.<cluster>). The existing wildcard SAN is preserved.
  • B2 (no restart trigger on cert-manager rotation) — fixed in 3e59b8491. Pod annotations now include secret.reloader.stakater.com/reload: "<release>-tls" when TLS is enabled. Targeted form (not the broad reloader.stakater.com/auto), so unrelated Secret churn doesn't trigger rolls. stakater/reloader is installed cluster-side via packages/system/reloader/.

Non-blocking follow-ups:

  • FU1 (additionalVolumes/additionalVolumeMounts unconditionally overwritten) — unchanged. ApplicationDefinition currently doesn't expose these fields to tenant input, so practical risk is zero today; would need merge-instead-of-overwrite if those fields are ever exposed.
  • FU2 (CA privateKey algorithm uniformity) — addressed via series-wide ECDSA unification below.
  • FU3 (schema null tightening) — addressed via series-wide schema cleanup below.

Series-wide cleanups applied here too:

  • Schema cleanup (1192ae69c): template now uses canonical kindIs "invalid" for tri-state null-detection. Schema source @param {*bool} emits plain "type": "boolean"; the prior array-type form is gone.
  • Private keys unified to ECDSA P-256 for both CA and leaf in 7544cdbf8. Cert chain reissue on first reconcile; existing trust anchors must be refreshed. Combined with B2 above, Qdrant pods pick up the new cert via Reloader-triggered restart.

Ready for re-review.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Each finding from my prior NOT LGTM on this branch is addressed in a dedicated commit:

  • 8a426487c fix(qdrant): add per-replica short-form SANs for p2p TLS hostname verification — closes the SAN gap I flagged: initialize.sh connects peers via <release>-N.<release>-headless (no FQDN), so the per-replica short form {{ printf "%s-%d.%s-headless" $.Release.Name $i $.Release.Name }} now sits in the SAN list alongside the namespaced and FQDN variants and the wildcard.
  • 3e59b8491 feat(qdrant): trigger pod restart on TLS Secret rotation via Reloader annotationsecret.reloader.stakater.com/reload: "{{ .Release.Name }}-tls" makes Stakater Reloader bounce pods when the leaf Secret rotates (cert-manager's rotationPolicy: Always). Solves the "cert rotated but pods still hold the old material" failure mode.
  • 7544cdbf8 refactor(qdrant): switch CA and leaf private keys to ECDSA P-256 + 1192ae69c refactor(qdrant): use kindIs invalid pattern for tri-state tls.enabled detection + fc943ddda refactor(qdrant): extract TLS enablement logic into _helpers.tpl — modernization pass consistent with the rest of the batch.

Leaf usages [digital signature, key encipherment, server auth, client auth] ✓. dashboard RBAC exposes only <release>-apikey (no TLS Secret), so the B3 pattern doesn't apply here either. Clean.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels May 27, 2026
- Extend QdrantSpec with tls.enabled tri-state bool and expose it via values.yaml and JSON schema
- Update cozyrds integration to pass TLS config to the operator
- Add test: Makefile target and TLS parameter documentation to README

Signed-off-by: Arsolitt <arsolitt@gmail.com>
- Add certmanager.yaml to provision cert-manager Certificate and Issuer for p2p and client TLS
- Extend qdrant.yaml to mount TLS secrets and configure Qdrant with TLS settings when enabled
- TLS activation follows a tri-state: explicit true/false or inherit from cluster external setting

Signed-off-by: Arsolitt <arsolitt@gmail.com>
- Add helm-unittest suite for cert-manager resources covering all TLS mode combinations
- Add helm-unittest suite for Qdrant StatefulSet verifying TLS volume mounts and p2p behavior
- Cover explicit enabled/disabled and external-inherited cases; document p2p standalone behavior

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Address review feedback: the TLS-enablement block was duplicated in both
qdrant.yaml and certmanager.yaml. Introduce a named helper template
"qdrant.tls.enabled" in _helpers.tpl and replace both inline copies with
a single include call.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…ification

Qdrant's upstream chart constructs bootstrap/uri using short-form pod
hostnames (<release>-N.<release>-headless). The existing wildcard SAN
only covered FQDN form, causing TLS handshake failures in multi-replica
clusters when ca_cert peer verification is enabled.

Add a range loop that emits three SAN variants per replica:
- short form: <release>-N.<release>-headless
- svc form: <release>-N.<release>-headless.<ns>.svc
- FQDN form: <release>-N.<release>-headless.<ns>.svc.<cluster-domain>

Scale-up beyond initial replicas requires cert re-issue; Reloader
(see next commit) will restart pods to pick up the new cert.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
… annotation

cert-manager rotates the leaf cert approximately every 11 months
(renewBefore: 720h, duration: 8760h). Qdrant reads tls.cert/key/ca_cert
at process start and does not watch for file changes, so without a
restart trigger TLS would silently break at the renewal boundary.

Add a targeted Reloader annotation (secret.reloader.stakater.com/reload)
pointing at the specific TLS Secret. This uses the targeted form, not the
auto-watch form, to limit scope to the TLS Secret only. Reloader is
installed cluster-side at packages/system/reloader.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…d detection

Replace the hasKey + index approach with the canonical kindIs "invalid"
check. This is the standard cozystack pattern for detecting an unset
*bool field, making the helper consistent with other charts in the
project.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
RSA keys are unnecessarily large for internal PKI. ECDSA P-256 provides
equivalent security with smaller key material, faster TLS handshakes,
and smaller certificates — beneficial for high-frequency p2p connections.

CA retains rotationPolicy: Never (root CA stability).
Leaf uses rotationPolicy: Always (new key on every renewal).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — TLS via cert-manager is correctly wired for client REST/gRPC and inter-node p2p, both blockers from the earlier review (p2p short-form SANs and restart-on-rotation) are fixed and test-covered, and 58 helm-unittest cases pass.

Business context: adds TLS to the managed Qdrant service for client REST/gRPC (6333/6334) and inter-node p2p (6335) via a self-contained cert-manager chain in the tenant namespace, with tls.enabled falling back to external when unset.

Non-blocking follow-ups

  1. p2p SANs are enumerated for the current replicas only (packages/apps/qdrant/templates/certmanager.yaml:75-79). On a replicas N→M change the Certificate re-renders and the StatefulSet scales in the same reconcile with no ordering guarantee (podManagementPolicy: Parallel), so a new pod can attempt p2p TLS before the reissued secret propagates and reloader restarts — a transient scale-up stall, not a permanent break; fresh installs are unaffected. Optional hardening: a short-form wildcard SAN "*.{{ .Release.Name }}-headless" covers future replica names without enumeration.
  2. CA and leaf both use ECDSA P-256 (clean within this PR); a sibling cert-manager PR pins RSA-4096 for the CA. Worth a one-line confirmation that the cross-app divergence is intentional.
  3. The tri-state relies on kindIs "invalid" to detect the omitted case while the schema keeps tls.enabled non-nullable (a literal null is rejected before rendering). Sound, but tightly coupled to the schema staying non-nullable — a brief comment pinning that intent would harden it against a future schema relaxation.

- {{ .Release.Name }}-headless.{{ .Release.Namespace }}.svc
- {{ .Release.Name }}-headless.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}
- "*.{{ .Release.Name }}-headless.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}"
{{- range $i, $_ := until (int .Values.replicas) }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SANs are enumerated for the current replicas only. On a replicas N→M scale-up, new pod qdrant-M-1.<release>-headless can attempt p2p TLS before the reissued secret propagates and reloader restarts — a transient stall, not a permanent break. Optional: a short-form wildcard SAN "*.{{ .Release.Name }}-headless" (X.509 wildcard matches the leftmost label) covers future replica names without enumeration. Non-blocking.

@Arsolitt
Arsolitt (Arsolitt) merged commit 41bab0c into main May 29, 2026
11 checks passed
@Arsolitt
Arsolitt (Arsolitt) deleted the feat/tls-qdrant branch May 29, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature lifecycle/active Indicates that an issue or PR is actively being worked on by a contributor size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants