feat(opensearch)!: issue HTTP and Dashboards certificates from cert-manager - #2682
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughOpenSearch HTTP-layer TLS is added as a tri-state ChangesOpenSearch HTTP TLS Support
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello, 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
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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/apps/opensearch/tests/tls_test.yaml (1)
29-249: ⚡ Quick winAdd 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
failedTemplatecase 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
📒 Files selected for processing (13)
api/apps/v1alpha1/opensearch/types.goapi/apps/v1alpha1/opensearch/zz_generated.deepcopy.gohack/check-opensearch-readme.batspackages/apps/opensearch/Makefilepackages/apps/opensearch/README.mdpackages/apps/opensearch/templates/_tls.tplpackages/apps/opensearch/templates/certmanager.yamlpackages/apps/opensearch/templates/external-svc.yamlpackages/apps/opensearch/templates/opensearch.yamlpackages/apps/opensearch/tests/tls_test.yamlpackages/apps/opensearch/values.schema.jsonpackages/apps/opensearch/values.yamlpackages/system/opensearch-rd/cozyrds/opensearch.yaml
ba2af88 to
1703b15
Compare
There was a problem hiding this comment.
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
- The cert-manager leaf secret
opensearch-<name>-http-cert(including the server private keytls.key) is surfaced to the tenant via the cozyrdssecrets.includelist (packages/system/opensearch-rd/cozyrds/opensearch.yaml:35-36). Namespace-scoped, not a cross-tenant escalation — but clients only needca.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. - 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.
|
Addressed your review. Blockers:
Non-blocking follow-ups:
Series-wide cleanups applied here too:
Ready for re-review. |
There was a problem hiding this comment.
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 winAdd 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"]). Thetlsobject 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
📒 Files selected for processing (5)
packages/apps/opensearch/templates/certmanager.yamlpackages/apps/opensearch/tests/tls_test.yamlpackages/apps/opensearch/values.schema.jsonpackages/apps/opensearch/values.yamlpackages/system/opensearch-rd/cozyrds/opensearch.yaml
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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-nhnvflag 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.
ab33611 to
3867793
Compare
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
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.
72ebf70 to
b1afd9a
Compare
|
Both blockers are closed at 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 The field is 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 |
1b091ba to
fdf76e1
Compare
b55881a to
dcd77af
Compare
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]>
dcd77af to
76c9ece
Compare
|
Head is The Dashboards selector: I said the chart already selected the label the operator sets, so nothing changed. That was backwards. The chart asked for 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. 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 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: |
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
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.
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.localwhile 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.issuerselects who issues the HTTP server certificate,cert-manageroroperator, and is named for the issuer because TLS is served under both. Unset resolves fromexternal(cert-manager for externally published services, the operator for cluster-internal); an explicit value always wins.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.<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>-caand is not touched.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, removestls.issuerso it resolves fromexternal, and asserts the cert-manager chain, the CR droppinghttp.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 privilegedinit-sysctlcontainer 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 theopensearch-operatorpackage that raisesvm.max_map_countto 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 explicitfalse, 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
PreparePodForDeleteexcludes 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 emptystorageClasstoreplicatedat one replica, and the PR lane runs LINSTOR without DRBD and creates onlylocal, so the E2E fixture takes its class from thestorageClassvalue the harness passes with--set-stringfromCOZY_E2E_STORAGE_CLASS, the way the vminstance fixtures do; on the nightly QEMU lane that still resolves toreplicated. And the external Dashboards Service selected labels no pod carries.pkg/builders/dashboards.goat v2.8.0 stampsopensearch.cluster.dashboards: <cluster name>on the Deployment, its selector and the pod template, and nothing else; the chart asked foropster.io/opensearch-clustertogether withapp.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.caSecretso 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 fromcluster.localon 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: v1with 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
TenantProjectionsentinel that names<release>.http-caas its source; the CA-extraction controller publishes aca.crt-only copy as<release>.tenant-ca, and the ResourceDefinition selects it with the engine-agnosticinternal.cozystack.io/tenant-calabel. The sentinel is gated on the same condition as the chain, so a plaintext release renders no sentinel instead of one stuck atSourceNotFound. Tenants get what they need to verify the server and nothing more.Known limitations
These are stated rather than left to be discovered:
<svc>.<ns>.svc.<DNS_BASE>withDNS_BASEleft at the vendoredcluster.local, so the job waited on a name that never resolved whichever CA had signed; the platform now renders that value fromnetworking.clusterDomainand it is on main, which is what unblocks this branch.tls.issuerfrom 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.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 withenableCertificateOwnerRef: falseand deleting aCertificatedoes not delete itsSecret.packages/apps/opensearch/README.mdcarries both, with the commands.Downstream repositories
packages/apps/opensearch/values.schema.jsongains thetlsobject, socozystack/terraform-provider-cozystackneeds the field modelled forcozystack_opensearch; tracked in cozystack/terraform-provider-cozystack#41.Upgrade impact
An existing
external: truerelease withtls.issuerunset 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. Settingtls.issuer: operatorbefore the upgrade keeps that half as it is. Releases that are not external are untouched. Every existing release rolls once regardless oftls.issuerandexternal, because the pod template loses the operator'sinit-sysctlcontainer; 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.issuersays, because the Services are gated onexternalalone: 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