feat(opensearch): add TLS support for HTTP API and Dashboards - #2682
feat(opensearch): add TLS support for HTTP API and Dashboards#2682Arsolitt (Arsolitt) wants to merge 43 commits into
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 Poem
🚥 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
| */}} | ||
| {{- define "opensearch.tls.enabled" -}} | ||
| {{- if kindIs "invalid" .Values.tls.enabled -}} | ||
| {{- .Values.external | default false | toString -}} |
There was a problem hiding this comment.
Auto-enabling chart-managed HTTP TLS from external: true makes a mode the chart documents as breaking securityadmin (cross-trust gap between transport CA and <release>-http-ca) the default for external deployments. Security-config reconciliation (users/roles/audit) silently stops applying — the latest commit documents a workaround (tls.enabled: false) but doesn't change the default. Gate this behind an explicit tls.enabled: true opt-in, or resolve the CA-bundle overlap, before auto-on is safe.
There was a problem hiding this comment.
The cross-trust gap is closed rather than gated. The CR now passes the chart CA to the operator as spec.security.tls.http.caSecret: <release>-http-ca, so the operator signs the securityadmin admin certificate with that same CA and anchors the HTTP listener on it — both sides end up on one anchor instead of two, and the security config applies. No operator bump needed; the field exists in the pinned 2.8.0.
One real constraint came out of reading the operator source: adminCAName() only honours http.caSecret at OpenSearch >= 2.0.0 and silently falls back to the transport CA below it. So on version: v1 the old mismatch would come back with no error anywhere. The chart refuses to render chart-managed HTTP TLS there — explicit tls.enabled: true fails with the mechanism named, auto-on degrades to operator-managed instead of breaking an existing external release on upgrade.
Your two follow-ups are also fixed: the tenant no longer receives the server private key (the ResourceDefinition selects a key-free ca.crt projection by label instead of listing the leaf Secret), and the CA usages are down to [signing, cert sign].
…otate Passing http.caSecret is what lets securityadmin verify the HTTP endpoint, but it also changes how the operator mounts the material: with a CA secret set it switches from mounting the whole secret directory to three individual mounts, each carrying SubPath. The kubelet never refreshes a subPath-mounted secret file, and nothing restarts the pods when the secret changes, so a renewal reaches the secret and stops there. A one-year leaf with a thirty-day renewBefore therefore promised a rotation that could not happen: cert-manager would reissue while the pods kept serving the old certificate until it expired outright, taking HTTPS on 9200 down and breaking securityadmin roughly a year after install. Give the leaf the same ten-year lifetime as the CA that signs it, drop the renewBefore that cannot be honoured, and say plainly that rotating it means deleting the secret and restarting the pods. Also document what the certificate means for a client: it is issued by a per-release private CA, delivered as the key-free <release>.tenant-ca secret, and the external names it covers are derived from the tenant host — which is what external-dns publishes, and not the in-cluster domain older releases used. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The trust anchor reaches the tenant through two different labels on two different objects: publish-ca-cert marks the key-bearing source the extraction controller reads, and tenant-ca marks the key-free projection it writes, which is what the ApplicationDefinition selects. Nothing enforced that separation. Because selection is purely by label, renaming one key on the CA Certificate's secretTemplate would have made the CA private key tenant-readable with every test in the repo still green — a one-line edit past a boundary the design calls deliberate. Pin it from both sides: the CA Certificate must not carry the projection's label, and the ApplicationDefinition must not select by the source's. Also give _namespace a default before dereferencing it. The existing check claimed to fail clearly when the tenant host is missing, but only covered _namespace present and empty; with the key absent entirely the render died on a raw nil pointer before the message could be emitted. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Three corrections, all where the previous wording would have misled someone making a security decision. The external access note claimed the published DNS names are the names the certificate carries SANs for. That is true only under chart-managed TLS. With TLS operator-managed — turned off explicitly, or fallen back to below OpenSearch 2.0.0 — the operator issues for in-cluster names only, so a client reaching the published name cannot verify it at all. Say which mode covers which names. Turning chart-managed TLS off does not withdraw the trust anchor. Helm removes the certificates but cert-manager keeps their Secrets, so the CA keeps matching the publish label and the tenant keeps receiving it, while the endpoint has already moved back to an operator-issued certificate under a different CA. Nothing the chart renders can prevent that — it does not own the Secret — so name the manual cleanup instead of leaving it to be discovered. The nil guard on tls was commented as handling an explicit null. Helm coalesces that back to the default in values.yaml, so nil never reaches the template and no test can exercise it; the comment now says it is defensive only. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
… bad usage Enabling chart-managed TLS replaces the certificate the operator was issuing, so any name the operator covered and the chart does not stops verifying the moment TLS goes on. The leaf was missing three: <release>.<namespace>, <release>-dashboards.<namespace>, and the <release>-discovery service. The two-label form is the one that matters most — with ndots:5 and the default search list it is what a client in another namespace actually resolves, and therefore what its TLS verification runs against. This was reachable without anyone opting in: with external access on and the switch unset, the tri-state turns TLS on by itself, so an upgrade alone would have narrowed the coverage of a working release. Also drop key encipherment from both certificates. RFC 5480 section 3 forbids it for ECDSA keys, which perform no key transport, and both keys here are ECDSA P-256 — it was left over from when they were RSA. The tenant-exposure check now asserts the shape of secrets.include rather than grepping for one spelling of a name. It is the only thing standing between the CA private key and the tenant, and the grep passed a leak written any other way; every entry must now be either the credentials Secret or the key-free tenant-ca projection. Correct the external-dns note while here: external-dns runs upsert-only, so re-applying publishes the new name but never withdraws the old record. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
… needed Two things a user has to know that the tri-state documentation did not say. An existing external release has the switch unset and runs operator-managed TLS today, so it moves to chart-managed TLS the first time it reconciles after this ships. That rolls the cluster and re-anchors it on a new CA — anything pinned to the operator's CA has to be repointed. The choice is deliberate and matches what a new release gets, but it must not be discovered by watching a data cluster restart, so say it, and pin the resolution in a test of its own rather than leaving it to incidental coverage. The manual restart was framed as something rotation needs. It is wider than that: the certificate is mounted by subPath and never refreshed in place, so changing external access, the tenant host or the cluster domain also reissues it while the pods keep serving the old one, with no error to notice. Say that under both parameters, since external is the one a user actually touches. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The check that no key-bearing Secret reaches the tenant iterated over whatever the selector returned, and a loop over nothing is a green test. Restructure spec.secrets.include, or run where yq is not installed, and it reported ok while checking nothing at all — on the one assertion standing between the CA private key and the tenant. Require yq up front, and assert the entry count before judging the entries, so the list being what we think it is becomes part of the claim rather than an assumption underneath it. Also correct what the release-name guard says it protects. Certificate, Issuer and Secret names are DNS subdomains bounded at 253, which nothing here comes near; the real limits are the 63-char DNS label on the SANs, where <release>-dashboards binds at 52, and the 64-char X.509 common name, which binds at 56. The 47-char guard sits under all of them and is unchanged — only the reasoning was wrong, and someone relaxing it later would have started from a premise that is not true. Pin both sides of the boundary now, not just the rejected one. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
grep exits non-zero when it counts nothing, so under set -e the count assertion killed the test before its own message could say what was expected. The check still failed, which is what matters, but it failed as a bare grep trace instead of naming the invariant it was defending. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…oint One combined leaf covered both services, so the Dashboards pod was mounted a private key whose SANs included every OpenSearch API name — the in-cluster ladder and, with external access on, the public hostname. Dashboards is the more exposed of the two and the more likely to be compromised, and anything that took it could then present a valid certificate for the API endpoint. The ten-year lifetime the subPath mount forces makes that a ten-year window. The operator never did this: it issues <cluster>-http-cert and <cluster>-dashboards-cert separately, so collapsing them was a widening this chart introduced. Issue one certificate per service instead, both from the same CA so a client trusting the published anchor still verifies both, each carrying only its own names. The Dashboards certificate renders last and only when Dashboards is enabled, so the other four keep their positions. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Splitting the certificate left four statements describing the design it replaced, one of them with an operational consequence. The manual procedures named only the API certificate. Changing external access, the tenant host or the cluster domain reissues both, so an operator following them literally restored the API endpoint and left Dashboards serving a certificate that no longer matched its own name — the silent failure the paragraph exists to prevent. It now covers both sides, and says the Dashboards deployment restarts separately, since it is not an OpenSearch pod. The disable-path cleanup likewise left the Dashboards Secret behind. The template header still described one leaf covering both services, directly contradicting the explanation further down the same file, and the external access note still spoke of a single certificate carrying both names. The Dashboards lifetime comment justified itself by the subPath mount, which is the one thing that does not apply there: the operator mounts a provided Dashboards certificate as a whole volume, so the kubelet does refresh it. The ten years are right for a different reason — Dashboards reads the files once at startup and nothing restarts it when they change. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ing has The API leaf is pinned three ways: it may not take a name the operator generates into, it may not be offered to the CA-extraction controller as a CA source, and it may not carry the other service's names. The dashboards certificate inherits every one of those hazards and had only positive assertions, so all three failures passed the suite green — including undoing the split itself by putting the API names back on the key the more exposed pod holds. Mirror the three, and the ECDSA usages assertion with them, so each fails against the corresponding regression instead of documenting the safe shape and checking none of it. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ly what it must not The guards added for the dashboards certificate were all prohibitive, so it ended up well defended against being the wrong thing and unchecked on being the right one. Pointing its issuerRef at the bootstrap self-signed Issuer instead of the shared CA passed the whole suite: cert-manager still issues, Dashboards still serves HTTPS, the release still goes Ready, and the anchor the platform publishes silently stops verifying that endpoint. So did switching the key to RSA, reintroducing the short lifetime that expires in place, and pointing it at the API certificate's Secret so two Certificates race over one object while the CR names only one of them. Mirror the structural assertion the API leaf already has — kind, secret name, issuer, key algorithm, size and rotation policy — and extend the lifetime pin. That also establishes that the document really is the dashboards certificate, without which the prohibitive guards would pass against whatever landed at that position if the order ever moved. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…tificate Neither leaf pinned isCA, and no other assertion can stand in for it. certSign is derived from that field rather than from spec.usages, so flipping a leaf to isCA: true renders an unchanged usages list, satisfies the exact-length and no-certSign pins, and still issues a working intermediate CA — one whose private key lives in the pod and chains to the anchor tenants are handed. That is a wider hole than the key-sharing the per-service split closes: it signs for any name, not just the sibling service's. issuerRef was pinned by name but not by kind, and on the dashboards leaf only. A reference that resolves to a different object of the same name still issues, still serves, and still goes Ready while anchoring on something the published trust anchor does not cover — the same silent failure already argued for the dashboards leaf, which applies just as well to the more exposed API endpoint and to the CA itself. Pin kind and group everywhere the name is pinned. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The comparison was described as running against the resolved image tag. It runs against the resolved version string — the same value that goes into spec.general.version, which is the field the operator branches on. That distinction is the reason the floor is correct: an images.opensearch override changes the running image without moving spec.general.version, so it cannot put the operator on one side of 2.0.0 while the chart believes the other. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
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 — app.kubernetes.io appears nowhere in it — 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. While here, correct what the SAN comment claims about parity with the operator. The fully-qualified rung is deliberately different: the operator builds it from its own DNS_BASE, left at the upstream cluster.local default, while the chart uses the cluster domain the platform runs on. The chart's name is the one that resolves — the difference is a fix, not a gap. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Listing the anchor in the ApplicationDefinition was only half the wiring. That entry feeds the lineage webhook, which labels the Secret as a tenant resource for the registry; it is not an access grant. Direct read comes from the chart's own Role, and that still named only the credentials Secret — so a tenant with use access got 403 on the very anchor this change exists to hand them. kafka carries both legs for its CA Secrets; this brings opensearch in line. Only the key-free projection is granted. The cert-manager CA and both leaves hold a private key and must never appear in that list, so the new test asserts the grant is exactly two names and that none of the three key-bearing Secrets is among them. The bats case named itself after exposing the CA to the tenant while asserting only the label; it now says which half it checks and points at the other. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
… empty Requiring the tenant host turned a supported platform state into a failed release. A cluster with no root host leaves that value empty all the way down the tenant tree, and Helm's required rejects the empty string, so external: true stopped rendering — not just the external-dns annotation but the whole chart, taking the OpenSearchCluster CR, the credentials Secret and the RBAC with it. A running release would have gone not-Ready on its next reconcile, and it fired with chart-managed TLS off too, reaching users who had opted out of this feature entirely. The same configuration renders on main. Degrade instead. The LoadBalancers still get addresses and still serve; the external-dns hostname is simply not published when there is no name to publish, and the certificate keeps its in-cluster SANs and omits the external ones for the same reason. Both are asserted, including that the cluster itself still renders, which is the part whose loss actually hurt. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The empty-host case rendered with dashboards disabled, so it only ever reached the API certificate. Breaking the guard on the dashboards leaf alone left the suite fully green while that certificate emitted a trailing-dot SAN — on the more externally exposed of the two services. Enable dashboards in that case and assert the fifth document the same way as the fourth. Breaking either guard alone now fails. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…uire The guard rejected any release name over 47 characters. The platform's own cap is 53, so names between the two are reachable, render today, and stopped rendering here — including on the auto-on path, where a release that never asked for chart-managed TLS would have stopped templating entirely and taken its HelmRelease NotReady. Nothing was violated at those lengths. The binding constraint is the 63-char DNS label on the SANs, and which SAN binds depends on configuration: with Dashboards enabled it is <release>-dashboards at 52, otherwise <release>-discovery at 53. The old message even printed the evidence, blaming a dashboards SAN that is not emitted when Dashboards is off. Compute the limit from the longest label actually built, and name that label in the message. The only rejected case left is one where the certificate genuinely could not be issued. Both boundaries are pinned from both sides, and the long-named auto-on release is pinned as still rendering. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The bound was derived from certificate SAN labels alone, which missed the longest name the chart produces: the <release>-dashboards-external Service, at 20 characters of suffix. Service names are DNS-1035 labels capped at 63, unlike the Certificate and Secret names the reasoning was based on, which are subdomains capped at 253 and never bind. So the real limit with external access and Dashboards both on is 43, and relaxing the guard to 52 made a configuration the API server rejects reachable — the Service is refused and the release fails with nothing in the chart to explain why. The bound also has nothing to do with TLS. That Service renders on external access alone, so a guard living only in the certificate template could not see the case at all. Move the check into a helper that picks the longest suffix the current values actually build — the external Dashboards Service, else the Dashboards name, else the discovery SAN, else the external Service — and invoke it from both templates, so it applies with chart-managed TLS off as well. Each boundary is pinned from both sides, and the message names the object that binds. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The guard ran from the certificate and external Service templates, so a release with Dashboards enabled but external access and chart-managed TLS both off reached no invocation. The operator still creates a <release>-dashboards Service for it — a DNS-1035 label bounded at 63 — so a 53-character release rendered clean and then failed at the API server with no chart-level error. That is the same shape as the external Service gap closed previously, on the axis that was left behind. Invoke the guard from the cluster template, which renders unconditionally and therefore covers every remaining path. The helper is idempotent, so the paths that already checked are unaffected. Also drop the limits table left behind in the certificate template. It still described a 52/53 bound with no mention of the external Service, contradicting the helper that now owns those numbers — and a stale second copy of that reasoning is exactly what produced the original mistake. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
a6ee81b to
01fd204
Compare
index on an absent map is a hard render error rather than a nil result, so the default that follows it never applied and the whole chart failed to render instead of the one value being read. The platform always injects _cluster, which is why this only showed up rendering the chart alone. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…rong The RBAC comment pointed at kafka as carrying both a registry entry and a read grant. Kafka grants its two CA secrets by name and has no label selector at all, so a reader following that pointer found a counter-example rather than the pattern being described. The name table called -discovery a TLS-only SAN label. The operator creates that Service on every path, so the name exists with TLS off too. Record which bounds Helm's own 53-character release-name limit leaves reachable while the table is being corrected. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Two suites in the TLS file carried no name, so both reported as a blank PASS line and a failure in either gave no clue which one broke. The readme check named for asserting the parameter appears outside the TLS section only asserted that it appears at all; the section half is the preceding test. Name it for what it does — it is the guard that stops that preceding test passing on a README that dropped the row entirely. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…tlives the pin The comment justified the matched lifetime entirely by the subPath mount shape, which is a property of the pinned operator rather than of OpenSearch: upstream has since moved those branches back to whole-directory mounts, so a routine vendored-chart bump would quietly retire the stated rationale while the conclusion stayed correct. State the version-independent reason first — certificates are read once at startup and no pod restarts on change — and mark the mount-shape half as pinned-version detail. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
|
Closing out the remaining feedback. Four fixes pushed; three findings did not survive checking. Correction to an earlier claim in this thread. It was stated that the gap could not be closed from the chart side because "the upstream operator currently exposes no API to provide a merged CA trust bundle". That is wrong, and it is what kept this open longer than it needed to be. Fixed:
Not changed, with reasons:
Two limitations in the PR body stand as written and are not addressed here: the operator's |
What this PR does
Adds TLS for the OpenSearch HTTP API (9200) and Dashboards (5601), issued by cert-manager in the tenant namespace, and hands the CA to tenants as a key-free trust anchor.
tls.enabledis nullable with tri-state semantics: unset defaults to the value ofexternal(on for externally published services, off for cluster-internal); an explicit value always wins.external: true.<release>-http-*so they cannot collide with the operator's own<release>-catransport CA. Reusing that name made the operator read the cert-manager Secret and fail, because it expectsca.keywhile cert-manager writestls.key.transport.generate: true,perNode: true) — the security plugin requires it and the chart cannot turn it off.securityadmin cross-trust
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 is fixed by handing the HTTP CA to the operator through
spec.security.tls.http.caSecret, which makes it sign the admin certificate with that same CA. 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 no longer exposed to tenants. The CA Certificate is instead labelled so the CA-extraction controller can publish a
ca.crt-only projection per release, which the ResourceDefinition selects with the engine-agnosticinternal.cozystack.io/tenant-caselector. Tenants get what they need to verify the server and nothing more.Known limitations
These are stated rather than left to be discovered:
DNS_BASEis stillcluster.localwhile the platform runs on a different cluster domain, so the securityadmin job waits on a name that does not resolve and never starts — independently of which CA signed the admin certificate. Closing that requires a change in the operator package, alongside the SANs of every operator-issued certificate. Tracked as follow-up.externalortls. Unit coverage pins the rendered chain, the guards and the SAN set, but the end-to-end handshake is not asserted in CI.Release note