Skip to content

feat(opensearch)!: issue HTTP and Dashboards certificates from cert-manager - #2682

Merged
Aleksei Sviridkin (lexfrei) merged 8 commits into
mainfrom
feat/tls-opensearch
Sep 11, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 8 commits into
mainfrom
feat/tls-opensearch

Conversation

@Arsolitt

@Arsolitt Arsolitt (Arsolitt) commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

The OpenSearch HTTP API (9200) and Dashboards (5601) already served TLS, from the operator's own private CA and with a SAN built from cluster.local while the credentials Secret advertised the release on the platform domain, so nothing a client was handed could verify. Both certificates now come from a per-release cert-manager CA in the tenant namespace, and the CA reaches tenants as a key-free trust anchor.

  • tls.issuer selects who issues the HTTP server certificate, cert-manager or operator, and is named for the issuer because TLS is served under both. Unset resolves from external (cert-manager for externally published services, the operator for cluster-internal); an explicit value always wins.
  • Renders a self-contained cert-manager chain in the tenant namespace: a self-signed Issuer, a CA Certificate, a CA Issuer, and two leaf Certificates, one for the HTTP API and one for Dashboards, each with the SANs of its own Service and the external hostname when external: true. Leaf keys are written as PKCS8, since the JVM loader rejects the PKCS1/SEC1 encoding cert-manager emits by default, and the CA subject is pinned so the operator's signature check accepts the chain.
  • The CA Secret is <release>.http-ca. The dot separates the chart-chosen suffix from release names, so no other release can produce that name; the two leaf Secrets stay hyphenated because none of the chart's suffixes is a tail of another. The operator's own transport CA remains at <release>-ca and is not touched.
  • Transport mTLS (9300) stays operator-managed (transport.generate: true, perNode: true). The security plugin requires it and the chart cannot turn it off.
  • hack/e2e-chainsaw/opensearch/ runs the move on a cluster: it installs the app on the operator's HTTP listener, removes tls.issuer so it resolves from external, and asserts the cert-manager chain, the CR dropping http.generate, the admin certificate reissued under the chart CA, the StatefulSet rolling onto the three subPath mounts, the tenant anchor, and the certificate the endpoint actually presents to a client holding that anchor.

Sysctl carrier and single-node rolls

The E2E suite for this PR was red because the chart set spec.general.setVMMaxMapCount: true, which makes the operator prepend a privileged init-sysctl container to every OpenSearch pod. Tenant namespaces run at the cluster's default PodSecurity level, baseline on Talos, so admission refused the container and the StatefulSet never produced a pod (#4131). The carrier for the sysctl is now on main: #4152 added a DaemonSet to the opensearch-operator package that raises vm.max_map_count to a floor of 262144 on every node, from a namespace the platform labels privileged. This branch is rebased onto that main and sets the flag to an explicit false, with a template comment saying why and a unit test pinning it, so a release no longer asks the operator for a container admission refuses. The value stays with the DaemonSet until the node contract carries it (#4144); the DaemonSet's own template comment, which said the chart still asks, now says it does not.

Three more things came out of the same pass. The chart asked the operator to drain data nodes before every rolling restart, and with a single data node the drain never finishes: at v2.8.0 PreparePodForDelete excludes the node from allocation and then waits for it to hold no shards, which the only data node can never satisfy, so a one-replica release could not roll for a version bump or for the certificate move this PR relies on. The chart now drains only when a second data node exists, pinned both ways in the unit tests. The chart maps an empty storageClass to replicated at one replica, and the PR lane runs LINSTOR without DRBD and creates only local, so the E2E fixture takes its class from the storageClass value the harness passes with --set-string from COZY_E2E_STORAGE_CLASS, the way the vminstance fixtures do; on the nightly QEMU lane that still resolves to replicated. And the external Dashboards Service selected labels no pod carries. pkg/builders/dashboards.go at v2.8.0 stamps opensearch.cluster.dashboards: <cluster name> on the Deployment, its selector and the pod template, and nothing else; the chart asked for opster.io/opensearch-cluster together with app.kubernetes.io/component: dashboards, so the LoadBalancer came up with no endpoints and no error to show for it. It selects the label the operator sets now.

securityadmin cross-trust, still blocked on the DNS base

Chart-managed HTTP TLS previously broke the operator's securityadmin reconciliation: the admin certificate was signed by the transport CA while the HTTP listener trusted the cert-manager CA, so the two never verified against each other and users and roles silently never applied. This chart puts one CA under both sides, handing the HTTP CA to the operator through spec.security.tls.http.caSecret so that it signs the admin certificate with the CA the listener trusts. That closes the CA mismatch. The other half, the job resolving a name the operator built from cluster.local on a cluster that does not use it, was closed separately and is on main, so the security configuration applies once this lands. No operator bump is required; the API exists in the pinned version.

That path is version-gated upstream at OpenSearch 2.0.0: below it the operator falls back to the transport CA and the mismatch returns with no error anywhere. The chart therefore refuses chart-managed HTTP TLS on version: v1 with an explicit render-time failure that names the mechanism and offers the alternatives, rather than letting it fail silently at runtime.

Trust anchor for tenants

The HTTP server's private key is not exposed to tenants. The chart renders a TenantProjection sentinel that names <release>.http-ca as its source; the CA-extraction controller publishes a ca.crt-only copy as <release>.tenant-ca, and the ResourceDefinition selects it with the engine-agnostic internal.cozystack.io/tenant-ca label. The sentinel is gated on the same condition as the chain, so a plaintext release renders no sentinel instead of one stuck at SourceNotFound. Tenants get what they need to verify the server and nothing more.

Known limitations

These are stated rather than left to be discovered:

  1. securityadmin needed both trust and reachability, and both are closed now. This branch put one CA under the admin certificate and the HTTP listener. The other half was the operator resolving <svc>.<ns>.svc.<DNS_BASE> with DNS_BASE left at the vendored cluster.local, so the job waited on a name that never resolved whichever CA had signed; the platform now renders that value from networking.clusterDomain and it is on main, which is what unblocks this branch.
  2. The e2e suite reaches the move, not the chart bump that triggers it. The suite above arrives at the chart-managed chain by removing tls.issuer from a running release, which leaves exactly the values an upgraded release arrives with. The one step it cannot run is the one where the previous chart version wrote the CR, because there is no second chart version to install from in this tree. Everything downstream of that write is exercised on a cluster.
  3. A reissue does not reach a running pod. Neither service reloads its certificate, and cert-manager reissues on any change to external, the tenant host or the cluster domain, so an ordinary edit leaves the pods serving material that no longer matches their own names with no error, event or condition to show for it. Completing such a change is manual. So is removing the CA private key cert-manager leaves behind when TLS is turned off again, since the platform runs with enableCertificateOwnerRef: false and deleting a Certificate does not delete its Secret. packages/apps/opensearch/README.md carries both, with the commands.

Downstream repositories

packages/apps/opensearch/values.schema.json gains the tls object, so cozystack/terraform-provider-cozystack needs the field modelled for cozystack_opensearch; tracked in cozystack/terraform-provider-cozystack#41.

Upgrade impact

An existing external: true release with tls.issuer unset serves operator-issued HTTP TLS today and moves to chart-managed TLS on its first reconcile after this lands: the cluster rolls once and is re-anchored on a new per-release CA, so any client pinned to the operator's CA has to be repointed at the key-free <release>.tenant-ca. Setting tls.issuer: operator before the upgrade keeps that half as it is. Releases that are not external are untouched. Every existing release rolls once regardless of tls.issuer and external, because the pod template loses the operator's init-sysctl container; on a single-replica release that roll is the first one the operator can complete, since draining the only data node used to block every rolling restart.

The published external-dns name changes on the same upgrade whatever tls.issuer says, because the Services are gated on external alone: they move from <release>.<namespace>.<cluster-domain>, an in-cluster name that never resolved outside the cluster, to <release>.<tenant-host> and <release>-dashboards.<tenant-host>. external-dns is configured upsert-only here, so the old record is not withdrawn and keeps pointing at the LoadBalancer until it is removed from the zone by hand. That cleanup is per zone rather than per release and nothing enumerates the records, so the README shows how to derive the list from the cluster.

Release note

feat(opensearch)!: the OpenSearch HTTP API and Dashboards already served TLS, from the operator's own private CA and with a SAN built from cluster.local while the credentials Secret advertised the release on the platform domain; both certificates now come from a per-release cert-manager CA whose anchor the platform publishes into the tenant as <release>.tenant-ca. Any client that worked against the old endpoint was skipping verification, because no hostname it was handed could have matched that SAN. Existing external releases move from the operator's CA to the chart's on their first reconcile, and their published external-dns name moves from the in-cluster domain to <release>.<tenant-host> with the old record left in place; set tls.issuer: operator beforehand to opt out of the CA move. With dashboards.enabled the application name is limited to 41 characters, and to 32 when external is on as well; past either bound the chart fails the render and withholds every object in the release. A release already above a bound is not broken by the guard, it is told why it has not been upgrading: the Dashboards Service the operator builds from that name exceeds the 63-character limit for a DNS label, the operator returns that failure on every pass, and the reconcilers queued behind Dashboards, which are upgrade and restart and snapshot repository, have not run since the release was created. Set dashboards.enabled to false to drop the bound. The external Dashboards Service also selected labels the operator does not put on Dashboards pods, so it stood up with no endpoints; it selects opensearch.cluster.dashboards now. The drain of data nodes is now requested only with the data role on and more than two replicas, since at two the operator gives up on a full drain itself. The chart stops asking the operator for its privileged init-sysctl container, since the opensearch-operator package now sets vm.max_map_count from a DaemonSet, so every existing release rolls once on upgrade; single-replica releases no longer ask the operator to drain their only data node, which used to block every rolling restart

@coderabbitai

coderabbitai Bot commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

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

OpenSearch HTTP-layer TLS is added as a tri-state tls.enabled value that defaults from external when omitted. Helm templates and a helper resolve the flag; cert-manager templates emit a self-signed Issuer, HTTP CA, CA Issuer, and combined leaf Certificate when enabled. CRD types, schema, tests, external service wiring, and docs are updated end-to-end.

Changes

OpenSearch HTTP TLS Support

Layer / File(s) Summary
CRD type contract and deepcopy support
api/apps/v1alpha1/opensearch/types.go, api/apps/v1alpha1/opensearch/zz_generated.deepcopy.go
ConfigSpec gains a new Tls TLS field, and a new exported TLS struct adds the tri-state Enabled *bool. Deepcopy methods are generated to handle the new field and its optional pointer semantics.
Helm values schema and defaults
packages/apps/opensearch/values.schema.json, packages/apps/opensearch/values.yaml
Helm chart values schema and defaults add a tls object with an enabled property that acts as a tri-state: when omitted/null, TLS follows the external setting; when explicitly set, it overrides that behavior.
TLS resolution template helper
packages/apps/opensearch/templates/_tls.tpl
New Helm helper opensearch.tls.enabled normalizes the tri-state Values.tls.enabled into a concrete boolean string, falling back to Values.external when unset.
Cert-manager certificate chain templates
packages/apps/opensearch/templates/certmanager.yaml
Conditional cert-manager resources render a 4-document chain when TLS is enabled: self-signed Issuer for bootstrapping, 10-year HTTP CA certificate with stable secret name, CA Issuer for leaf signing, and a unified 1-year leaf certificate for both OpenSearch HTTP and Dashboards HTTPS with combined SANs and ECDSA keys.
Cluster and external service TLS wiring
packages/apps/opensearch/templates/opensearch.yaml, packages/apps/opensearch/templates/external-svc.yaml
OpenSearchCluster spec.security.tls.http and spec.dashboards.tls switch between the cert-manager secret reference and operator-generated TLS based on the resolved TLS flag. External service templates require _namespace.host for external mode and use it to construct external-dns hostnames.
TLS test suite
packages/apps/opensearch/tests/tls_test.yaml
Tests validate cert-manager chain rendering across tri-state combinations, leaf certificate SANs for service DNS and optional external hosts, certificate usages, transport mTLS operator generation, and guard failures for misconfiguration.
Documentation and system schema updates
packages/apps/opensearch/README.md, hack/check-opensearch-readme.bats, packages/system/opensearch-rd/cozyrds/opensearch.yaml
README documents the tls.enabled tri-state behavior and parameters. Bats tests validate README content completeness and cozyrds YAML includes HTTP certificate secret in secrets.include. Cozyrds schema is extended with the spec.tls.enabled property and field ordering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: issuing HTTP API and Dashboards certificates through cert-manager for OpenSearch.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tls-opensearch

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.

@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
@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 introduces TLS support for the OpenSearch HTTP API and Dashboards, enabling secure communication via cert-manager. It implements a flexible tri-state configuration for TLS, allowing users to explicitly enable or disable it, or rely on automatic detection based on external access settings. The changes ensure that transport mTLS remains managed by the operator while providing a robust, self-signed certificate chain for the HTTP layer, complete with necessary SANs and validation logic.

Highlights

  • TLS Support: Added TLS support for the OpenSearch HTTP API and Dashboards using cert-manager, including a tri-state tls.enabled configuration.
  • Cert-Manager Integration: Implemented a self-contained cert-manager chain (Issuer, CA Certificate, CA Issuer, and leaf Certificate) with SAN policies covering internal and external DNS names.
  • Configuration Safety: Added a name-length guard to prevent Kubernetes object name collisions and failures when release names exceed 47 characters.
  • Testing: Introduced comprehensive BATS tests for README structure and Helm chart tests to verify TLS configuration states.
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/opensearch/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. ↩

@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 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 HTTP-layer TLS configuration for OpenSearch and Dashboards using cert-manager. It implements a tri-state logic for the tls.enabled field (auto-on when external access is enabled), adds corresponding cert-manager templates, and includes comprehensive unit and static tests. Feedback was provided regarding the placement of the TLS configuration section in values.yaml to prevent splitting the "Common parameters" table in the generated documentation.

Comment thread packages/apps/opensearch/values.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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/apps/opensearch/tests/tls_test.yaml (1)

29-249: ⚡ Quick win

Add a regression test for the 47-char release-name guard.

This suite validates TLS behavior well, but it does not currently lock in the new fail-fast contract for overly long release names. Adding one failedTemplate case here would prevent regressions.

Proposed test addition
 tests:
+  - it: fails when release name exceeds the cert-manager naming limit
+    release:
+      name: os-test-name-that-is-definitely-longer-than-forty-seven
+      namespace: tenant-test
+    set:
+      tls:
+        enabled: true
+    asserts:
+      - failedTemplate: {}
+
   - it: "(a) renders no resources when external false and tls.enabled unset"
     asserts:
       - hasDocuments:
           count: 0
🤖 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/opensearch/tests/tls_test.yaml` around lines 29 - 249, Add a
new failedTemplate test case to this tls_test.yaml suite that sets a release
name longer than 47 characters (e.g. a 48-char string) and asserts a
failedTemplate with an errorMessage indicating the release-name-length guard
(for example: "release name must be at most 47 characters" or the existing guard
message). Place it alongside the other TLS cases (near the top-level tests
block) and use the same structure as other cases (set: release: name:
"<48-char-string>" and asserts: - failedTemplate: errorMessage: "<expected
message>"). Ensure the assertion uses the existing failedTemplate symbol so the
CI will catch regressions to the 47-char guard.
🤖 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 `@hack/check-opensearch-readme.bats`:
- Around line 23-28: The test "topologySpreadPolicy is not in TLS configuration
section" uses `awk ... | grep -qv "topologySpreadPolicy"` which incorrectly
succeeds if any line differs; change the assertion to explicitly ensure the TLS
section does not contain the string by replacing the negative grep logic with an
explicit absence check (e.g., use the pipeline output piped into a command that
fails when "topologySpreadPolicy" is found, such as negating `grep -q
"topologySpreadPolicy"` or failing when `grep -q` matches). Update the command
in that test (the awk pipeline followed by the grep check) so it reliably fails
if `topologySpreadPolicy` appears in the TLS section.

In `@packages/apps/opensearch/README.md`:
- Around line 27-32: The README contains a duplicate heading "### Common
parameters" (first occurrence and a second before the table with
`topologySpreadPolicy` and `version`) which triggers MD024; rename the second
heading to a distinct title such as "### Common parameters (additional)" or "###
Additional parameters" so it no longer duplicates the earlier "### Common
parameters" heading, keeping the table and surrounding content unchanged.

---

Nitpick comments:
In `@packages/apps/opensearch/tests/tls_test.yaml`:
- Around line 29-249: Add a new failedTemplate test case to this tls_test.yaml
suite that sets a release name longer than 47 characters (e.g. a 48-char string)
and asserts a failedTemplate with an errorMessage indicating the
release-name-length guard (for example: "release name must be at most 47
characters" or the existing guard message). Place it alongside the other TLS
cases (near the top-level tests block) and use the same structure as other cases
(set: release: name: "<48-char-string>" and asserts: - failedTemplate:
errorMessage: "<expected message>"). Ensure the assertion uses the existing
failedTemplate symbol so the CI will catch regressions to the 47-char guard.
🪄 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: 07f954cc-c19f-420d-a077-ef3cab5e1100

📥 Commits

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

📒 Files selected for processing (13)
  • api/apps/v1alpha1/opensearch/types.go
  • api/apps/v1alpha1/opensearch/zz_generated.deepcopy.go
  • hack/check-opensearch-readme.bats
  • packages/apps/opensearch/Makefile
  • packages/apps/opensearch/README.md
  • packages/apps/opensearch/templates/_tls.tpl
  • packages/apps/opensearch/templates/certmanager.yaml
  • packages/apps/opensearch/templates/external-svc.yaml
  • packages/apps/opensearch/templates/opensearch.yaml
  • packages/apps/opensearch/tests/tls_test.yaml
  • packages/apps/opensearch/values.schema.json
  • packages/apps/opensearch/values.yaml
  • packages/system/opensearch-rd/cozyrds/opensearch.yaml

Comment thread hack/check-opensearch-readme.bats
Comment thread packages/apps/opensearch/README.md Outdated

@lexfrei Aleksei Sviridkin (lexfrei) 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.

NOT LGTM — external: true silently auto-enables a chart-managed HTTP TLS mode that the chart itself documents as breaking the operator's securityadmin job, so security-config reconciliation (users/roles/audit) stops applying on external deployments; this is documented, not fixed or gated.

Business context: add cert-manager-backed TLS for the OpenSearch HTTP API (9200) and Dashboards (5601) in the tenant namespace, leaving operator-managed transport mTLS (9300) untouched.

Blockers

B1: external: true auto-enables an HTTP TLS mode that breaks securityadmin reconciliation
File: packages/apps/opensearch/templates/_tls.tpl:13, packages/apps/opensearch/templates/opensearch.yaml:33-38
Issue: when tls.enabled is unset, the helper turns chart-managed HTTP TLS on whenever external: true. This switches the HTTP listener from the operator-generated cert to a cert-manager leaf signed by <release>-http-ca. The operator's securityadmin job connects to the HTTP listener with an admin cert and a CA bundle rooted in the transport CA (/certs/ca.crt), which does not include <release>-http-ca. The -icl/-nhnv flags relax cluster-name and hostname checks only, not CA-chain verification, so the handshake fails, the job exits non-zero, and <release>-security-config (users/roles/audit/password policies) is never applied or refreshed.
Evidence: opensearch.yaml:35 sets the HTTP secret to <release>-http-cert when TLS is on; certmanager.yaml:51-77 issues that leaf from <release>-http-ca, distinct from the operator transport CA <release>-ca. The chart's own KNOWN ISSUE / KNOWN LIMITATION comments (certmanager.yaml:18-23, values.yaml:101-110) acknowledge that securityadmin "cannot verify the HTTP TLS chain". The latest commit (a6ee81b) adds a workaround (tls.enabled: false) to the docs but does not change the auto-on default.
Impact: an existing external OpenSearch deployment silently loses security-config reconciliation upon upgrade, with no error surfaced to the operator the platform targets; auto-on via external makes a documented-broken mode the default for external deployments.
Fix (pick one): (a) do not auto-enable chart-managed HTTP TLS from external — require an explicit tls.enabled: true opt-in, keeping operator-managed certs as the external default until the cross-trust gap is resolved; (b) overlay <release>-http-ca's ca.crt into the admin job's trust bundle; or (c) sign the HTTP cert from the operator transport CA.

Non-blocking follow-ups

  1. The cert-manager leaf secret opensearch-<name>-http-cert (including the server private key tls.key) is surfaced to the tenant via the cozyrds secrets.include list (packages/system/opensearch-rd/cozyrds/opensearch.yaml:35-36). Namespace-scoped, not a cross-tenant escalation — but clients only need ca.crt/the public cert to trust the endpoint; exposing the private key is unnecessary. Compare kafka-rd which exposes a CA-only secret. Prefer a CA/public bundle.
  2. The CA cert usages include key encipherment (packages/apps/opensearch/templates/certmanager.yaml:57-60), a no-op for a CA; the conventional profile is [signing, cert sign, crl sign]. Cosmetic.

@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
@github-actions github-actions Bot added the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label May 26, 2026
@Arsolitt

Copy link
Copy Markdown
Contributor Author

Addressed your review.

Blockers:

  • B1 (securityadmin cross-trust gap) — documented as a known limitation in 30d25a616. Per the analysis, securityadmin's -icl -nhnv flags don't relax the CA chain check, and the upstream operator currently exposes no API to provide a merged CA trust bundle. Without that, this gap can't be closed from the chart side. The doc block in values.yaml next to the tls.enabled field flags this so operators understand the constraint before enabling chart-managed HTTP TLS. To revisit when upstream supports a merged trust bundle field.

Non-blocking follow-ups:

  • FU1 (tls.enabled: false falls through to operator-generated self-signed) — true; the off-branch produces operator-managed TLS, not plaintext. Not addressed in this PR; behavior unchanged.
  • FU2 (key encipherment unusual on CA) — CA usages unchanged in this pass. CA now uses ECDSA P-256 (see series-wide note below) and the unused bit is harmless; can be removed in a follow-up.
  • FU3, FU4 — informational, no change.

Series-wide cleanups applied here too:

  • Schema cleanup (43d7801e2): dropped ["boolean","null"] from tls.enabled. Template uses kindIs "invalid" for tri-state null-detection.
  • Private keys unified to ECDSA P-256 for both CA and leaf in ab33611ea. Cert chain reissue on first reconcile after upgrade; existing tenant trust anchors must be refreshed.

Ready for re-review.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/system/opensearch-rd/cozyrds/opensearch.yaml (1)

29-29: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add nested keysOrder entry for tls.enabled.

The keysOrder array follows a pattern where parent paths are followed by their important nested properties (e.g., ["spec", "images"] → ["spec", "images", "opensearch"], ["spec", "dashboards"] → ["spec", "dashboards", "enabled"]). The tls object contains only one property, enabled, which should be included in keysOrder for consistent dashboard UI field ordering.

📋 Proposed keysOrder fix
-    keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "topologySpreadPolicy"], ["spec", "version"], ["spec", "tls"], ["spec", "images"], ["spec", "images", "opensearch"], ["spec", "nodeRoles"], ["spec", "nodeRoles", "master"], ["spec", "nodeRoles", "data"], ["spec", "nodeRoles", "ingest"], ["spec", "nodeRoles", "ml"], ["spec", "users"], ["spec", "dashboards"], ["spec", "dashboards", "enabled"], ["spec", "dashboards", "replicas"], ["spec", "dashboards", "resources"], ["spec", "dashboards", "resourcesPreset"]]
+    keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "topologySpreadPolicy"], ["spec", "version"], ["spec", "tls"], ["spec", "tls", "enabled"], ["spec", "images"], ["spec", "images", "opensearch"], ["spec", "nodeRoles"], ["spec", "nodeRoles", "master"], ["spec", "nodeRoles", "data"], ["spec", "nodeRoles", "ingest"], ["spec", "nodeRoles", "ml"], ["spec", "users"], ["spec", "dashboards"], ["spec", "dashboards", "enabled"], ["spec", "dashboards", "replicas"], ["spec", "dashboards", "resources"], ["spec", "dashboards", "resourcesPreset"]]
🤖 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/system/opensearch-rd/cozyrds/opensearch.yaml` at line 29, The
keysOrder array is missing a nested entry for the tls.enabled property, causing
inconsistent UI ordering; update the keysOrder array (the keysOrder declaration)
to include ["spec","tls","enabled"] immediately after ["spec","tls"] so the tls
object’s enabled field is explicitly ordered like the other nested properties
(e.g., follow the pattern used for ["spec","images"] ->
["spec","images","opensearch"] and ["spec","dashboards"] ->
["spec","dashboards","enabled"]).
🤖 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/opensearch/values.schema.json`:
- Around line 146-147: The description for tls.enabled in values.schema.json
incorrectly mentions "null" as a tri-state value while the schema enforces
"type": "boolean"; update the source annotation in values.yaml for the
tls.enabled field to say "unset/omitted" (not "null") and then regenerate the
JSON schema using the generator (cozyvalues-gen / run make generate) so
values.schema.json is rebuilt from values.yaml rather than editing
values.schema.json manually.

In `@packages/apps/opensearch/values.yaml`:
- Around line 101-109: The workaround text is misleading about operator-managed
HTTP TLS: clarify that when using external: true (external deployment), leaving
tls.enabled unset enables chart-managed TLS, so to use operator-managed HTTP TLS
in external mode users must explicitly set tls.enabled: false; update the
paragraph to replace the suggestion "keep tls.enabled unset (operator-managed
mode)" with guidance "set tls.enabled: false for operator-managed HTTP TLS when
external: true" and mention the involved keys tls.enabled and external to make
the required configuration change explicit.

---

Outside diff comments:
In `@packages/system/opensearch-rd/cozyrds/opensearch.yaml`:
- Line 29: The keysOrder array is missing a nested entry for the tls.enabled
property, causing inconsistent UI ordering; update the keysOrder array (the
keysOrder declaration) to include ["spec","tls","enabled"] immediately after
["spec","tls"] so the tls object’s enabled field is explicitly ordered like the
other nested properties (e.g., follow the pattern used for ["spec","images"] ->
["spec","images","opensearch"] and ["spec","dashboards"] ->
["spec","dashboards","enabled"]).
🪄 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: 0240e1db-9017-41db-a1eb-b8364cbff38e

📥 Commits

Reviewing files that changed from the base of the PR and between af76d5f and ab33611.

📒 Files selected for processing (5)
  • packages/apps/opensearch/templates/certmanager.yaml
  • packages/apps/opensearch/tests/tls_test.yaml
  • packages/apps/opensearch/values.schema.json
  • packages/apps/opensearch/values.yaml
  • packages/system/opensearch-rd/cozyrds/opensearch.yaml

Comment thread packages/apps/opensearch/values.schema.json Outdated
Comment thread packages/apps/opensearch/values.yaml Outdated

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. My prior concern about the securityadmin cross-trust limitation is now explicitly documented:

  • 30d25a616 docs(opensearch): document securityadmin cross-trust limitation in values.yaml — the chart's leaf cert is signed by the chart's own CA, but the OpenSearch operator runs securityadmin against the HTTP listener using the transport CA that the operator manages separately. The -nhnv flag passed by the operator's securityadmin job skips hostname verification but still validates the chain, so a chart CA ≠ transport CA means the bootstrap apply may fail until the operator's transport CA is overlaid into the chart's trust. This is now called out at the values doc level so operators know what they're signing up for.
  • af76d5f19 test(opensearch): add failedTemplate case for the 47-char release-name guard + 15f97fc61 fix(hack): correct negative grep assertion in opensearch README bats test — the chart-side guard against names too long for the operator's StatefulSet-named-as-resource-name convention is now positive- and negative-tested.
  • ab33611ea refactor(opensearch): switch CA and leaf private keys to ECDSA P-256 + 43d7801e2 refactor(opensearch): drop null from tls.enabled schema, use kindIs invalid pattern — same modernization as the rest of the batch.

Leaf usages [server auth, signing, key encipherment] — signing is cert-manager's alias for digital signature (pkg/api/util/usages.go maps both to KeyUsageDigitalSignature). No client auth because the chart cert serves only the HTTP listener; transport-side mTLS is operator-owned with the separate transport CA. dashboard RBAC exposes only <release>-credentials, no TLS Secret leak. Clean.

Comment thread packages/apps/opensearch/templates/_tls.tpl Outdated

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verdict

NOT LGTM as of 76bb7cba3 — two things to settle, and one of them is a name that becomes expensive to change the moment this merges.

What was actually missing, which is not what the title says

Worth getting this straight first, because the title sent me looking for the wrong thing. TLS was not missing. On the merge base the chart already sets spec.security.tls.http.generate: true and spec.dashboards.tls: {enable: true, generate: true}, and templates/security.yaml already advertises https:// URIs in the credentials Secret. Both listeners were already serving TLS.

What was missing is a certificate anyone could verify. The cert came from the operator's own private CA, which no tenant can obtain, and its FQDN SAN was built from the operator's DNS_BASE=cluster.local while the Secret advertises <name>.<ns>.svc.cozy.local. So the URI the chart hands a tenant has never verified — not against a CA they could fetch, and not against a name in the SAN. This PR replaces the issuer with a per-release cert-manager CA, puts the real cluster domain in the SAN, and publishes the anchor as <release>.tenant-ca on the same contract the rest of the epic uses.

That is a better change than the title claims, and I would rather it said so. feat(opensearch)!: add TLS support for HTTP API and Dashboards describes adding a feature that was already there; this is an issuer change that makes the existing TLS verifiable. The release note opens the same way. Both worth correcting, because the next person deciding whether this affects them will read the title first.

Three things ride along that are not TLS and are not in the title either: setVMMaxMapCount flipping to false, drainDataNodes becoming conditional on replicas, and a Dashboards external-Service selector fix for a selector that matched no pod. The release note covers the first two.

Blocker 1: merge order, and it just got longer

The hard dependency on #4185 is real and this diff says so in three places. packages/system/opensearch-operator/values.yaml sets no manager.dnsBase, so the vendored default cluster.local applies. This PR closes the CA mismatch that securityadmin trips over, but the job still resolves a name composed from DNS_BASE, so the security configuration — users, roles, audit policies — still does not apply. Merged alone this ships a change whose headline benefit does not work.

The sequencing matters in the other direction too: when #4185 moves DNS_BASE, the operator-issued SANs move with it, and that is exactly the tls.enabled: false path here.

As of today #4185 is in rework at the author's own request, so this one waits on that rather than on review.

Blocker 2: tls.enabled names a switch that does not switch TLS

In this chart tls.enabled: false does not turn TLS off. It selects the operator's issuer — the else branch renders generate: true and the listener still serves TLS. So the field name states the opposite of what the field does, on a user-facing API.

This is not a general style objection; the fleet already made this call and wrote down the reasoning. packages/apps/mariadb/values.yaml:87 names its equivalent field issuer and says why, in as many words: "Named for the issuer because TLS is served under both, unlike the similarly-spelled tls.enabled in some other charts, which does switch TLS on and off." That comment exists to stop exactly this spelling from spreading, and this chart would become the counterexample it warns about. tls.issuer: operator | cert-manager matches mariadb precisely and reads true.

I am asking for this before merge rather than as a follow-up because the field is already in the ApplicationDefinition schema and there is a terraform-provider issue filed against it. Renaming afterwards costs a schema migration plus a second provider change; renaming now costs a sed.

The rest of the upgrade story, which is well handled

I went looking for the usual upgrade traps and did not find them. No Secret type changes, because every cert-manager Secret lands on a new name and none collides with an existing object — so no immutable-type wedge. No Service port change, no connection-string change (the URIs were already https:// and the leaf SAN covers the advertised name), no value renamed. The default flip is scoped: external: true with tls unset moves to the chart-managed CA, external: false does not move, and an explicit tls.enabled: false does not move.

Two things every release gets regardless of TLS, and the note covers both: the setVMMaxMapCount change rolls every pod once, and for external: true releases the external-dns name moves from the in-cluster domain to <release>.<tenant-host>, upsert-only, so the old record is orphaned rather than removed.

One omission in the note: the new release-name cap. opensearch.validateReleaseName fails the render for external plus dashboards beyond 43 characters. Such a release would previously have failed at apply on an invalid Service name, so it is not a new class of breakage — but it now withholds every object in the release, which is worth a line.

As for whether an existing client keeps working: only one that skipped verification, and given the SAN could not have matched the advertised name, that is every client that worked at all. Worth stating plainly in the note rather than leaving a reader to work out that the old path was never verifiable.

Notes, not blocking

Certificate lifetime is ten years with no renewBefore, on the CA and both leaves, against redis and mariadb's five-year CA and one-year leaf. The justification in certmanager.yaml is sound — nothing reloads the certificate, and at operator 2.8.0 the caSecret branch mounts with SubPath, which the kubelet never refreshes — and I could not find a hole in it. But the outcome is a ten-year server private key whose only rotation path is the manual delete-and-restart in the README, and that should be a decision somebody made rather than something that slipped through.

Inferring TLS from external is a deliberate departure from a rule a sibling wrote down: packages/apps/redis/values.yaml:98 says TLS is opt-in and not inferred from external. The practical effect here is milder than it sounds — flipping external swaps the CA rather than turning encryption on — and the reason is stated. Still worth one line saying the two charts differ on purpose.

secrets.exclude stays empty in the ApplicationDefinition while redis carries four entries and mariadb thirteen, both added under this epic as a named backstop behind the label-only include. There are three enumerable key-bearing Secrets here and none is listed. Not universal across the fleet, so a nit — but note that hack/check-opensearch-rd-secrets.bats pins the empty list, so adding entries later means editing the test too.

The chart's own LoadBalancer Service names are in no SAN, so a client dialling <name>-external by in-cluster name fails hostname verification. The operator's certificate did not cover it either, so no regression, and the e2e sidesteps it with an explicit -servername.

The cross-release Secret collision documented in certmanager.yaml:49-60 deserves its own issue rather than a comment: templates/users.yaml renders <release>-user-<username> with the username tenant-supplied and unconstrained by the schema, so release x with a user named http-server collides with release x-user. Both objects are Helm-owned, so the realistic outcome is the second release wedging on ownership rather than key disclosure — but it is cross-release and tenant-triggerable, which is the combination worth tracking.

drainDataNodes: {{ gt (int .Values.replicas) 1 }} counts total nodes rather than data nodes, so nodeRoles.data: false with three replicas still asks for a drain. Harmless, but the expression does not say what the comment above it means.

On comment volume: these are WHY comments and they satisfy the letter of AGENTS.md, but the dot-in-the-Secret-name argument alone is restated in certmanager.yaml, opensearch.yaml, values.yaml, the README and the bats file. certmanager.yaml is 341 lines of which roughly 190 are comment. Collapsing that to one canonical place would cut the cost of reading this diff more than anything else in it. Related: the DNS-label footnote now attached to dashboards.enabled ships into the ApplicationDefinition and becomes help text under an on/off toggle in the dashboard UI — that belongs in the README.

Checked and clean

The vendored-chart rule does not apply: packages/apps/opensearch/charts/cozy-lib is a symlink to the in-repo library chart, and the only file touched under packages/system/opensearch-operator/ is that package's own template directory, not its vendored charts/. Generated artifacts are consistent — the chart's values.schema.json is byte-identical to the ApplicationDefinition's openAPISchema, keysOrder gained ["spec","tls"] in the right place, the README table matches, and the deepcopy is regenerated. The package Makefile has a test: target, so hack/helm-unit-tests.sh actually discovers the suite — 11 suites, 140 tests, green here — and the assertions are non-vacuous: reverting the TLS templates turns the upgrade suite red on the notExists generate and Secret-name assertions. The chainsaw suite follows the repo conventions and its last step is the one that earns it, reading the certificate the listener actually serves and fingerprint-matching it against the chart's leaf. Tenant boundary holds: the anchor reaches the tenant only through the label selector, the dashboard Role grants the credentials Secret by name, and the e2e asserts the negative — the key-bearing Secrets are not served as tenant secrets.

Fix the field name and I have no objection to the rest; the merge still waits on #4185.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the feat/tls-opensearch branch 2 times, most recently from 72ebf70 to b1afd9a Compare September 10, 2026 12:31
@lexfrei Aleksei Sviridkin (lexfrei) changed the title feat(opensearch)!: add TLS support for HTTP API and Dashboards feat(opensearch)!: issue HTTP and Dashboards certificates from cert-manager Sep 10, 2026
@lexfrei

Copy link
Copy Markdown
Contributor

Both blockers are closed at b1afd9a71.

The title said the wrong thing and it is fixed, in the PR and in the lead commit subject, so the history does not carry the claim either. Both listeners already served TLS from the operator's CA with a SAN that could not match the name the credentials Secret advertised; what changes here is the issuer and the SAN.

Merge order cleared itself: the DNS base change is on main and this branch is rebased onto it, so manager.dnsBase now arrives from networking.clusterDomain through the shared values Secret and the securityconfig job resolves. The rebase had no conflicts, nothing on main since the merge base touches a file this branch does, and the branch diff is byte-identical before and after.

The field is tls.issuer now, values operator and cert-manager, matching mariadb down to the comment about why it is named for the issuer; the helper is opensearch.tls.certManagerIssued, so a boolean name does not survive one layer down either. Unset is the absence of the key rather than a null boolean, so tls: {} as shipped and tls: null from a hand-written values file both resolve from external, and an explicit value still wins. Seven mutations of the helper each turn the suite red, and one of them was worth having: | default dict used to be defensive and is load-bearing now, because index errors on nil where the old field read did not, so the test comment claiming the two null cases could not discriminate it was wrong.

Both omissions from the note are in: the 43-character cap with the part that matters, that the render fails whole instead of losing one Service at apply, and that any client that worked before was skipping verification.

One of your non-blocking notes turned out to be worth more than its wording. Reading util.DataNodesCount, it sums the live StatefulSets of pools carrying the data role, so nodeRoles.data: false really does mean zero data nodes and the old expression asked for a drain of nothing. At two data nodes the operator gives up on a full drain by itself, warns that some shards may not drain and then waits on system index primaries alone, which the surviving node cannot take while it holds their replicas. So the threshold moved to three as well: the predicate reads and .Values.nodeRoles.data (gt (int .Values.replicas) 2), with 1, 2, 3 and the role-off case each pinned. The rest of your notes are untouched.

Arsolitt (Arsolitt) and others added 8 commits September 11, 2026 01:23
The HTTP listener and Dashboards ran on certificates the operator minted
itself, so a client outside the cluster had no anchor it could obtain
through the platform and no way to verify the endpoint it was handed.

Issue the HTTP chain from cert-manager instead, and give the operator
the same CA through spec.security.tls.http.caSecret so it signs the
securityadmin admin certificate with it. Without that the admin
certificate would keep the operator's own CA while the listener trusted
cert-manager's, and securityadmin could no longer authenticate to apply
users, roles and audit policy.

tls.issuer names the authority rather than a switch, because the
listener serves TLS under either value and only the signer changes. It
is tri-state: an explicit value is obeyed, an absent one follows
external, because a release nobody publishes has no third party to
prove anything to. The cert-manager issuer needs OpenSearch 2.0.0 or
newer, since the operator honours the HTTP CA only from that version
and silently signs with its transport CA below it. An explicit choice
under that floor fails the render and one resolved from external falls
back to the operator, so asking for a verifiable certificate never
quietly yields less than was asked, and turning on external access
never breaks an upgrade.

BREAKING CHANGE: an externally published release on OpenSearch 2.0.0 or
newer serves the HTTP API from a cert-manager CA rather than the
operator's own. A client pinned to the operator CA must move to the
trust anchor the platform projects into the tenant.

BREAKING CHANGE: the external-dns name of an externally published
release moves from <release>.<namespace>.<cluster-domain> to
<release>.<tenant-host>, and the Dashboards name with it. This half is
gated on external alone and does not depend on the issuer. external-dns
runs upsert-only here, so the old record stays in the zone until it is
removed by hand.

BREAKING CHANGE: with dashboards enabled the application name is limited
to 41 characters, and to 32 when external access is also on. A release
above either limit now fails to render rather than converging. Such a
release was already stalled: the Dashboards Service built from its name
exceeds the 63-character DNS label limit, and the operator returns that
failure ahead of its upgrade, restart and snapshot-repository
reconcilers, so it has not upgraded or rolled since it was created. Set
dashboards.enabled to false to converge again, or recreate the
application under a shorter name.

Assisted-by: LLM
Signed-off-by: Arsolitt <[email protected]>
[lexfrei: reworded subject and body for the issuer field rename]
Signed-off-by: Aleksei Sviridkin <[email protected]>
The external Dashboards Service selected opster.io/opensearch-cluster
together with app.kubernetes.io/component. The operator labels Dashboards
pods with opensearch.cluster.dashboards and nothing else, so the selector
matched no pod and the LoadBalancer stood up with no endpoints and no
error to show for it.

That was survivable while nothing pointed at it. It is not now: the
certificate carries a SAN for the Dashboards hostname and the parameter
documentation describes it as a reachable external endpoint, so the chart
promises something the Service cannot deliver. Select the label that
exists, and pin it against the two that do not.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
…chor

A client inside the tenant needs the anchor that signs the HTTP chain,
and the only copy lives in the cert-manager CA Secret, which stores the
CA private key alongside it. Granting that Secret by name in the
resource map would convey the key with it, and would keep conveying
whatever later occupies the name after the platform has withdrawn the
verdict, so the one revocation channel the anchor has would be bypassed.

Project it instead: the controller copies ca.crt alone into a key-free
Secret the tenant roles already reach through tenantsecrets, gated on
the label the lineage webhook stamps. The private key never leaves the
release namespace, and withdrawing the label withdraws the anchor.

The projection is gated on the same condition as the certificates it
follows, since its source Secret exists only under the cert-manager
issuer; ungated it would sit SourceNotFound on every release the
operator signs for.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
Reading the cluster values through index aborts the whole release when
they are absent: index on an untyped nil is a hard render error, not a
miss returning nil, so the default written after it never runs. The
platform always injects them, so this surfaces only where the chart is
rendered on its own, which is where the unit suites and a bare helm
template both live.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
Most of what this chart decides about TLS is invisible in a rendered
manifest until a cluster disagrees with it. The upgrade case is the
sharpest: a release that already runs operator-managed HTTP TLS must
keep working when the chart takes the CA over, and nothing about the
rendered output says whether it will.

The negative assertions carry as much as the positive ones. The trust
anchor must stay out of the resource map, since a name grant there
would outlive the platform's verdict on it, and a null tls value must
leave the chart rendering rather than aborting the whole release. Both
are properties that fail silently and only a test states them.

The bats guard covers what helm-unittest cannot see: which Secrets the
opensearch-rd definition exposes, so a key-bearing one cannot be added
to that surface without the guard going red.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
No suite created an OpenSearchCluster anywhere in the tree, so the whole
class of failure where the chart renders correctly and the cluster then
refuses the workload had no coverage at all. Rendering proves the
manifests; only a cluster proves that securityadmin authenticates
against the CA the chart handed the operator and that the security
configuration actually applies.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
…tainer

spec.general.setVMMaxMapCount makes the operator append a privileged
init container to every OpenSearch pod, so that vm.max_map_count reaches
the 262144 the store needs. Tenant namespaces run at the cluster's
default PodSecurity level, and on Talos that is baseline, which refuses
the container before a pod exists: the StatefulSet never produces one
and the release never comes up.

The opensearch-operator package now raises the sysctl on every node
from a DaemonSet in its own namespace, which the platform labels
privileged, so the pod no longer has to ask. The flag is set to an
explicit false rather than dropped, and a unit test pins it, so the
next reader sees a decision and not an omission. Carrying the value in
the node contract would let the DaemonSet go; until then the DaemonSet
is the carrier.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
The chart asked the operator to drain data nodes before restarting them,
whatever the replica count. Draining moves a node's shards to another
data node and waits until none are left, and below three data nodes
there is nowhere for them to go: with one the operator requeues forever
and never deletes the pod, and with two it gives up on a full drain by
itself, warning that some shards may not drain and then waiting for the
system index primaries alone, which the surviving node cannot take while
it holds their replicas. Such a release could not complete any rolling
restart the operator drives: a version bump, a configuration change, or
the move of the HTTP listener onto a cert-manager certificate.

Drain only where a spare data node exists. Without draining, the
operator pauses replica allocation for the restart and re-enables it
once every pod runs the new revision, which is the path a small cluster
can take. The predicate also reads the data role, since the operator
counts data nodes over pools that carry it and a release with the role
off has none. Every boundary is pinned in the unit tests.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei

Copy link
Copy Markdown
Contributor

Head is 76c9ece4a. Two corrections to what I told you above, both mine.

The Dashboards selector: I said the chart already selected the label the operator sets, so nothing changed. That was backwards. The chart asked for opster.io/opensearch-cluster together with app.kubernetes.io/component: dashboards, and pkg/builders/dashboards.go puts neither on a Dashboards pod, so the external Service came up as a LoadBalancer with no endpoints and nothing anywhere to say so. The branch does fix it, and the fix now has its own commit rather than sitting inside the lead one.

The release-name bound: I gave you 43 for external plus Dashboards and said the old behaviour was that only the Dashboards Service got rejected. The second half was wrong and it changed the first. DashboardsReconciler runs ahead of upgrade, restart and snapshot repository, and the reconciler loop returns on the first error, so a Service the API refuses for its name length stops those three on every pass, quietly, for the life of the release. So the name is now guarded rather than left to fail late: with Dashboards enabled the application name is bounded at 41 characters, and at 32 when external is on too. That refuses exactly one value the API otherwise allows, and dashboards.enabled: false drops the bound. A release already past it is not broken by the guard, it is told why it has not been upgrading.

Also in since your review, each with a test: the first two commits did not render at all, because the helper defining the release-name check arrived two commits after three templates started calling it; six of the new security assertions passed against documents that do not carry the field they assert on, and now select by field rather than by index; the remediation guard in the chainsaw suite treated an empty history as healthy, where its sibling caller already fails closed; and the tls.issuer description had the causality backwards, claiming the cert-manager issuer is what lets the security configuration apply, when at v2.8.0 the operator issuer already puts one CA under both sides. What that issuer actually buys is the external hostname in the SAN and an anchor the platform can hand the tenant.

One thing your notes asked about that I can now answer with a number rather than a preference. You asked whether the ten-year certificate lifetime was a decision somebody made. What it replaces on the external path is a one-year leaf that nothing renews: pki.go:152 mints leaves at a year against the CA's ten, and the three generate paths key on the Secret being absent, so the only certificate the operator reissues by age is the admin one. Filed as #4211.

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM as of 76c9ece4a.

Both blockers are closed. #4185 is merged and this branch sits directly on its merge commit, e89617d75, which is the merge-base here, so the securityadmin job now resolves on the platform domain rather than cluster.local. And tls.enabled is tls.issuer with operator and cert-manager, carrying the same reasoning mariadb wrote down, so the field no longer names a switch that does not switch TLS off. The title says what the change does now too.

The version floor is the part I checked hardest, since it decides what an upgrade does to a release nobody is watching. _tls.tpl resolves the tri-state the way the comment says: explicit value obeyed, absent one follows external, and below 2.0.0 an explicitly requested cert-manager fails the render while an inferred one degrades to operator. That asymmetry is right. Someone who asked for a per-release CA should hear that they cannot have it on that version; someone who only asked for external access should keep the release they already have.

Two things I had flagged last time I am withdrawing. The certificate lifetime now carries the argument rather than just the number, including that dropping renewBefore does not stop reissue since cert-manager still defaults to duration/3, and that a 30-day cycle was a promise nothing here could honour. And secrets.exclude being empty reads differently now that include is an allowlist: one entry by resourceNames for the credentials Secret and one label selector that matches the key-free projection by design, so there is no broad label include for an exclude list to backstop.

What I would still like to know is how the release-name bound was confirmed. The guard withholds every object in the release, and its premise is that a release past the bound was already stuck: the operator composes the Dashboards Service past the 63-character label limit, fails on every pass, and the reconcilers queued behind Dashboards have not run since the release was created. If that holds, the guard turns a silent stall into a message, which is worth a lot. If it does not, the guard turns a working release into a failed one. The operator's Go source is not vendored here, only its chart, so I cannot check dashboards.Reconcile ordering at 2.8.0 from the tree, and the chainsaw suite does not exercise a name past either bound. A line saying how you established it, or one e2e case at 42 characters, would close it.

One follow-up worth its own issue rather than more prose in values.yaml. Changing external, the tenant host or the cluster domain reissues both certificates while the pods go on serving the old material, with no error anywhere, and the fix is a manual delete plus restart on each side. Nothing in the chart detects that state: there is no checksum annotation over the certificate, so nothing rolls the pods and nothing reports the mismatch. The documentation is thorough about it, which is the right first move, but a condition that is invisible until a client fails verification will be found by the client.

The suite is 12 files and 144 tests, green here, and the generated artifacts line up: values.schema.json and the ApplicationDefinition's openAPISchema are identical. Nothing under any charts/ directory is touched, so the vendored-chart rule does not come into it.

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/breaking-change Indicates the change introduces a breaking API or behaviour change 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.

4 participants