Skip to content

feat(tls): TLS for RedisFailover via cert-manager - #85

Open
lexfrei wants to merge 40 commits into
freshworks-oss:masterfrom
lexfrei:feat/tls-support
Open

lexfrei wants to merge 40 commits into
freshworks-oss:masterfrom
lexfrei:feat/tls-support

Conversation

@lexfrei

@lexfrei lexfrei commented Sep 1, 2026 •

Copy link
Copy Markdown

New optional spec.tls. With it on, Redis and Sentinel close the plaintext port, listen on tls-port, and mount the cert read-only at /tls, at mode 0440 when the pod has an fsGroup (without one the kubelet leaves the projected files root:root and a non-root container could not read them, so the default mode stays). Every redis-cli the operator generates (shutdown and readiness scripts, exec probes) gets --tls --cert --key --cacert, and the redis_exporter sidecars get the matching REDIS_EXPORTER_TLS_* env and dial rediss://.

The cert comes from one of two places, and validation rejects both or neither. spec.tls.certManager.issuerRef points at an Issuer or ClusterIssuer and the operator creates the Certificate itself. spec.tls.certificateSecret.secretName points at a Secret managed elsewhere. Validation also rejects a caCertSecretName that names the TLS Secret itself, in either mode, because publishing the CA-only Secret there would overwrite the serving certificate.

SANs cover the four Service names in short, namespaced, .svc and .svc.<domain> form, a wildcard for the headless per-pod records so changing the replica count does not re-issue, and localhost plus 127.0.0.1/::1 for the in-pod self-dials (probes, exporters, the Sentinel monitor target) that would otherwise fail verification. The domain comes from a new --cluster-domain flag defaulting to cluster.local, since hardcoding it breaks every cluster running a different one. spec.tls.certManager.extraSANs appends anything else; entries parse as IP or DNS automatically.

The operator's own Redis and Sentinel client dials TLS as well, so check-and-heal keeps working against a TLS-only failover. It validates with the CA from the cluster Secret and a ServerName override, because pod IPs are unstable but the headless service name is in the SAN list. Under spec.tls.authClients: yes it also presents the leaf cert and key from the same Secret, so mutual TLS is usable instead of a setting that locks the operator out.

The operator also publishes a second Secret holding only ca.crt, named <tls-secret>-ca by default and overridable via spec.tls.caCertSecretName as long as it does not collide with the TLS Secret. A client that verifies the server needs the CA and nothing else; handing it the issued Secret hands it tls.key too. This one can be granted on its own. It defers quietly until ca.crt appears, since cert-manager fills the Secret asynchronously.

TLS is chosen when the RedisFailover is created and cannot be turned on or off afterwards, which the CRD enforces with a transition rule on the spec. Redis replication and Sentinel cannot straddle two wire protocols: a replica restarted onto the other protocol cannot sync from the master, and Sentinels restarted onto it declare the master down and fail over to that replica while the master is still taking writes. An absent tls block and enabled: false are the same state, so adding a disabled block to a plaintext failover is fine; issuerRef, caCertSecretName, extraSANs and authClients stay mutable. A dual-listener transition, both port and tls-port open while replication moves across, would make the flip safe and is not part of this.

The operator also holds off on the ConfigMaps, StatefulSet and Deployment until the TLS Secret exists, so a first install with cert-manager does not create the pods and then roll them again once the certificate lands.

Renewal rolls the pods. On every reconcile the operator stamps a hash of the Secret's tls.crt and ca.crt as redis-failover.freshworks.com/tls-secret-hash on the pod template of both the Redis StatefulSet and the Sentinel Deployment, taken from the Secret read it already does for the CA-only publish. Redis is an OnDelete StatefulSet, so the operator's own roller sees the new revision and replaces one pod per reconcile; the Sentinel Deployment rolls itself. Redis loads the certificate and the CA bundle once at startup and re-reads neither, which is why a change to either has to restart the pod. tls.key is not part of the hash: it rotates with the certificate, and there is no reason to derive a published value from private key material.

The chart's ClusterRole gains create, get and update on certificates in cert-manager.io and on secrets, the verbs the code calls and no others. It does not gain list, watch, patch or delete: the operator reads one Secret by name and never enumerates them, and the CA-only Secret goes away through its owner reference.

spec.tls is absent by default and every code path sits behind TLSEnabled(rf), so existing failovers are untouched. That is pinned, not assumed: the probes dial localhost only under TLS (the bare $(hostname) is not in the SAN list) and keep the upstream $(hostname) form otherwise, a test holds the plaintext probe commands to the v3.3.5 literals, and a dump of the generated StatefulSet, Deployment, Services and ConfigMaps across eight failover shapes with spec.tls absent differs from v3.3.5 by zero bytes, so an operator upgrade does not roll workloads whose spec did not change. The two CRD schema tests skip when a tree carries no generated CRD at all, for consumers that vendor the operator as source only. Two dependency changes come with it: github.com/cert-manager/cert-manager v1.18.5 for the typed API and clientset, and k8s.io/apiextensions-apiserver from v0.28.4 to v0.32.0 because that clientset needs it. golang.org/x/text moves to v0.39.0 as well, to clear the govulncheck finding on an indirect dependency master already carries. cert-manager itself does not have to be installed: with nothing opting into TLS the operator never touches the Certificate API.

Origin: Arsolitt wrote this in a now-archived downstream fork of spotahome/redis-operator that the cozystack project used to run. I cherry-picked rather than rewrote so the authorship survives. Adapting it here meant routing the TLS flags and probes through the engine-aware CLI binary from EngineFor() so Valkey gets valkey-cli, keeping the DisableMyMaster branch in the sentinel template and rf.MasterName() in the sentinel probe, threading the new *tls.Config after the masterName parameters this repo already carries, and pinning cert-manager on the 1.18 line because a newer pin drags k8s.io/api, apimachinery and client-go up with it. That pin is also why issuerRef is typed cmmeta.ObjectReference, the same struct cert-manager 1.19+ renamed to IssuerReference. The renewal roll, the name-collision check and the mount mode are additions on top of the port.

The immutability rule is tested by handing the committed CRD's x-kubernetes-validations to the apiserver's own CEL engine and validating old/new objects against it, rather than restating the rule in Go where the two could drift. That is what pulls k8s.io/apiserver, cel-go and their transitive requires into go.mod as indirect. They are test-only: the module set of go list -deps ./cmd/... is the same 54 modules before and after, and the image build runs go build.

Testing: go build ./..., go test ./... and golangci-lint run -E goimports are green, the last two in the repo dev image, the integration suite compiles under -tags integration, and govulncheck reports no affecting vulnerabilities. helm lint and helm template pass. New unit tests cover the redis and sentinel config directives, the volume and mount layout on both pods including the 0440 mode with and without an fsGroup, the exporter env, the probe and script flags, the Secret name precedence and the rejection of a CA Secret name colliding with the TLS Secret in all three forms, the Certificate's SANs and usages under both the default and a custom cluster domain, the CA-only Secret carrying ca.crt and neither tls.crt nor tls.key, the renewal hash on both pod templates (present with TLS on, absent with it off, changing when tls.crt or ca.crt changes, user annotations preserved, the informer's map never mutated), and BuildTLSConfig across empty CA, missing ServerName, malformed PEM, client cert loaded and cert-without-key. The CRD is regenerated with controller-gen 0.17.3 and the deepcopy with code-generator 1.33.0, the versions ghcr.io/slok/kube-code-generator:v0.7.0 bundles; the chart is bumped to 3.4.0 for the version-check job. Mocks are hand-written in the generated style because go generate ./mocks does not run here: mockery 2.45.0 fails under the Go version in go.mod with internal error: package "fmt" without types.

Arsolitt and others added 22 commits September 1, 2026 17:25
Introduce spec.tls to opt into TLS for the Redis failover cluster.
TLS is provisioned either by cert-manager (operator creates a
Certificate CR) or by referencing an existing Secret, never both.

CRD shape:

  spec.tls.enabled           bool
  spec.tls.authClients       "no" | "optional" | "yes" (default "no")
  spec.tls.certManager.issuerRef          cmmeta.ObjectReference
  spec.tls.certManager.secretName         string  (optional override)
  spec.tls.certManager.duration           *metav1.Duration
  spec.tls.certManager.renewBefore        *metav1.Duration
  spec.tls.certManager.privateKey         *cmapi.CertificatePrivateKey
  spec.tls.certificateSecret.secretName   string

Validation:
- enabled=true requires exactly one of certManager / certificateSecret
- certManager.issuerRef.name is required; kind defaults to "Issuer"
  and group to "cert-manager.io" when empty
- certificateSecret.secretName is required
- authClients must be one of "no"/"optional"/"yes" when set

Deps:
- github.com/cert-manager/cert-manager v1.18.2 (typed CRD API; the
  operator never requires cert-manager to be installed at runtime
  when TLS is disabled, the import only brings in Go types)

The operator does not yet consume the new fields. Subsequent commits
plumb the settings through to the Redis/Sentinel pods, the issued
Certificate resource and the operator's own redis client.

Ported from a downstream fork of spotahome/redis-operator. Adapted:
the cert-manager pin is v1.18.2 rather than a 1.20.x release so that
k8s.io/api, apimachinery and client-go stay on the versions this repo
already uses; issuerRef is therefore typed cmmeta.ObjectReference,
which is the same struct cert-manager 1.19+ exposes as IssuerReference.
k8s.io/apiextensions-apiserver moves from v0.28.4 to v0.32.0 because
cert-manager's clientset requires it. Deepcopy functions come from the
repo's own generator rather than the fork's hand-written ones.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
When spec.tls.enabled is true the operator now emits a configuration
that drives Redis (server, replication and Sentinel traffic) entirely
over TLS:

- redis.conf and sentinel.conf get a TLS block (port 0, tls-port, the
  three /tls/{tls.crt,tls.key,ca.crt} paths, tls-auth-clients from
  spec, tls-replication yes). The plaintext port is closed
- The Redis StatefulSet and Sentinel Deployment mount the TLS Secret
  read-only at /tls. The Secret name is resolved with a clear
  precedence: spec.tls.certificateSecret.secretName takes priority,
  then spec.tls.certManager.secretName, then a generated default
  (rftls-<name>) used in the cert-manager-managed path
- The shutdown.sh and ready.sh scripts in the operator-managed
  ConfigMaps inject the matching --tls --cert --key --cacert flags
  into every redis-cli invocation
- The inline exec probes (liveness on the redis container, both
  probes on the sentinel container) get the same flags
- The redis_exporter sidecars receive REDIS_EXPORTER_TLS_* env vars
  pointing at the mounted certificate; REDIS_ADDR switches to
  rediss:// so the exporter dials TLS as well

The operator does not yet create the cert-manager Certificate
resource; that lands in a separate commit. spec.tls.certificateSecret
(bring-your-own) is already wired through.

New tests under generator_tls_test.go cover the config files, the
volume + mount layout, the exporter env, the probe scripts and the
GetTLSSecretName precedence rules. With TLS disabled all existing
test fixtures continue to pass unchanged.

Ported from a downstream fork of spotahome/redis-operator. Adapted
to this codebase: the TLS flags go through the engine-aware CLI
binary and the auth env name resolved by EngineFor(), so Valkey
failovers pick up valkey-cli; the sentinel config template keeps the
DisableMyMaster branch and gains the TLS block ahead of it; the probe
for the sentinel master address keeps rf.MasterName(); and the new
tests stub GetStatefulSet, which EnsureRedisStatefulset now calls for
maxmemory validation.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
…vers

When spec.tls.enabled is true and spec.tls.certManager is set, the
operator now creates (and reconciles) a cert-manager Certificate that
issues into the Secret the pods already mount. The Certificate is a
no-op for the bring-your-own-secret mode (spec.tls.certificateSecret)
and obviously for TLS-disabled failovers.

Wiring:
- service/k8s: new Certificate service interface and CertificateService
  implementation talking to cert-manager's typed clientset; added to
  Services so existing call sites pick it up via embedding
- cmd: CreateKubernetesClients now also builds the cert-manager client;
  main.go passes it into k8s.New
- service generator: generateRedisCertificate builds a Certificate with
  the IssuerRef from spec.tls.certManager, the resolved Secret name,
  optional duration / renewBefore / privateKey overrides, and the
  ServerAuth + ClientAuth key usages
- ensurer: EnsureRedisCertificate runs before the ConfigMaps and pods
  so the Secret cert-manager populates exists by the time pods mount it

SAN coverage on the issued certificate:
- the four Service names (redis headless, redis master, redis slave,
  sentinel) each in short, namespaced, .svc and .svc.cluster.local form
- a wildcard against the redis headless service so per-pod DNS records
  (<pod>.rfr-<name>.<ns>.svc.cluster.local) validate without re-issuing
  when replica count changes
- no IP SANs — pod IPs are unstable; the operator client validates
  pod-IP-targeted dials via ServerName override against the headless
  DNS SAN

Mocks for Services and RedisFailoverClient carry the new methods.
They are written by hand in the generated style because `go generate
./mocks` cannot run: mockery 2.45.0, the version this repo pins,
fails under the Go toolchain in go.mod with "internal error: package
\"fmt\" without types".

Tests:
- EnsureRedisCertificate creates a Certificate with the right SANs,
  issuer ref, secret name, duration/renewBefore and usages
- EnsureRedisCertificate is a no-op when TLS is disabled
- EnsureRedisCertificate is a no-op for bring-your-own-secret mode
- The existing TestEnsure suite now stubs EnsureRedisCertificate

The operator's own redis client still dials plaintext, so a failover
with TLS enabled is not yet reconcilable end to end; teaching
service/redis/client.go to speak TLS is separate work.

Ported from a downstream fork of spotahome/redis-operator. Adapted to
this codebase: cmmeta.ObjectReference in place of IssuerReference to
match the cert-manager version pinned here, and the mock additions
described above.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Introduce an optional caCertSecretName on spec.tls. It names the Opaque
Secret the operator publishes with only ca.crt, letting clients verify
the server under narrowly scoped RBAC without access to any private key.

Ported from a downstream fork of spotahome/redis-operator. Adapted: the
CRD schema is not touched here. The fork regenerated its manifests in a
separate commit that is not part of this port, so a single regeneration
covering the whole of spec.tls follows this commit instead.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The secret service could only read secrets. Add create/update and a
create-or-update helper mirroring the configmap service so the operator
can manage secrets it owns. Extend the Services mock and cover the create
and update paths with a unit test.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
When TLS is enabled the operator extracts ca.crt from the cluster TLS
secret and publishes it into a dedicated Opaque Secret (default
<tls-secret>-ca, overridable via spec.tls.caCertSecretName). The Secret
never holds a private key, so RBAC can be scoped to it for client trust
distribution. Works in both cert-manager and bring-your-own modes and
defers gracefully until ca.crt is available. Widen the chart RBAC so the
operator can manage secrets.

Ported from a downstream fork of spotahome/redis-operator. Adapted to
this codebase: the fork's version of this change sat on top of a
--cluster-domain flag and an extended SAN helper that are not part of
this port, so the CA-secret code is grafted onto the SAN helper and
the three-argument client constructor that exist here.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
CreateOrUpdateSecret compares the stored Secret's type and data against
the desired state and returns early when they match, avoiding a
redundant API write (and resourceVersion bump) on every reconcile of
the owning resource. The operator does not watch Secrets, so this is a
churn-reduction optimization rather than a loop fix.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Run the repository's CRD generator over the new TLS fields so the
manifest, the kustomize base and the chart CRD all carry spec.tls.
Generated with controller-gen 0.17.3 and crd:crdVersions=v1, which is
what ghcr.io/slok/kube-code-generator:v0.7.0 runs; that image has no
arm64 build, so controller-gen was invoked directly at the same version
and its output is byte-identical to the committed CRD apart from the
new block.

Bump the chart to 3.4.0, which the version-check job requires whenever
the CRD manifest changes.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The operator's own redis/sentinel client (service/redis/client.go)
now dials TLS when the failover has spec.tls.enabled.

Approach: every Client method gains a final *tls.Config parameter
that is fed straight into rediscli.Options.TLSConfig. A nil config
preserves the historical plaintext behavior, so disabled-TLS clusters
are unaffected.

Plumbing:
- service/redis/tls.go: new BuildTLSConfig helper. Loads the CA out
  of the ca.crt key of the cluster Secret (the one cert-manager or
  the user populates), produces a tls.Config with the supplied
  ServerName for SAN-based validation, MinVersion TLS 1.2, no
  InsecureSkipVerify.
- operator/redisfailover/service/check.go: new tlsConfigFor helper
  reads the Secret named by GetTLSSecretName and feeds BuildTLSConfig
  with ServerName = GetRedisName(rf) (the redis headless service,
  which is the SAN every issued cert covers).
- check.go and heal.go: every method that already loaded the password
  now also calls tlsConfigFor and passes the resulting config to the
  client. tlsConfigFor returns nil for TLS-disabled failovers so the
  pre-existing call sites keep their plaintext semantics.

Interface changes:
- service/redis.Client: each method gains a trailing *tls.Config
- service.RedisFailoverCheck.CheckSentinelMonitor: gains a leading
  *redisfailoverv1.RedisFailover so tlsConfigFor has access to it
- service.RedisFailoverHeal.RestoreSentinel: same, leading rf

Callers in operator/redisfailover/checker.go updated to pass rf.

Why ServerName, not InsecureSkipVerify: pod IPs are unstable, but the
issued cert covers the headless service DNS name. Dialing pod IP +
ServerName override gives strong validation against the SAN list
without juggling IP SANs that would churn on every reschedule.

Mocks (mocks/service/redis/Client.go, the two RedisFailover* mocks)
are updated by hand to match the new signatures. The check_test and
heal_test fixtures are updated en masse (mock.Anything for the new
trailing tlsConfig arg). New tests cover BuildTLSConfig: nil/empty
CA, missing ServerName, malformed PEM, success path.

Ported from a downstream fork of spotahome/redis-operator. Adapted to
this codebase: the *tls.Config goes on after the masterName parameter
this repo already threads through MonitorRedisWithPort,
GetSentinelMonitor, SetCustomSentinelConfig and SentinelCheckQuorum,
and CheckSentinelMonitor derives masterName from rf.MasterName()
rather than taking it separately.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The integration suite builds the operator's clients directly, so the
cert-manager client added to CreateKubernetesClients and k8s.New and
the trailing *tls.Config on the redis client reach it too.

Ported from a downstream fork of spotahome/redis-operator. Adapted:
this repo has a second integration file and a second client setup in
creation_test.go that the fork did not, and its GetSentinelMonitor
call also carries masterName.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
…xample

The operator now requires write access to cert-manager Certificate
resources whenever a RedisFailover opts into TLS with the
spec.tls.certManager block. Grant create/delete/get/list/patch/
update/watch on cert-manager.io/certificates from the operator's
ClusterRole. The rule is always present; the operator only issues
requests when a failover has TLS enabled, so installations that do
not have cert-manager installed remain unaffected.

Add example/redisfailover/tls.yaml that demonstrates the cert-manager
managed path and documents the bring-your-own-Secret alternative in
a comment.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Adds spec.tls.certManager.extraSANs: a list of additional Subject
Alternative Names appended to the service and per-pod SANs the operator
already computes. Entries are auto-classified by net.ParseIP — IPv4 and
IPv6 strings become IP SANs, everything else becomes a DNS SAN. The
merge is purely additive so in-cluster connectivity is preserved when
operators add external hostnames or LoadBalancer VIPs for off-cluster
clients.

The CRD is regenerated in all three published locations: chart crds,
kustomize base and manifests root.

Ported from a downstream fork of spotahome/redis-operator. Adapted: the
deepcopy and CRD output stay on this repo's generators (deepcopy-gen
style and controller-gen 0.17.3) instead of the newer controller-gen
the fork had migrated to, and the go directive is left alone.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
tlsConfigFor only verified that the ca.crt key existed in the TLS
Secret, not that its value was non-empty. When the key is present but
empty, redis.BuildTLSConfig returns (nil, nil), which callers interpret
as "no TLS" and dial in plaintext, silently downgrading the connection
even though spec.tls.enabled is true. Fail fast instead.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The operator only loaded the CA bundle from the TLS Secret, so its
internal Redis client could reach a server running without mutual TLS
(tls-auth-clients: no) but had no way to satisfy one running in mutual
mode: the handshake is rejected because there is no client certificate
to validate.

BuildTLSConfig now takes optional certPEM/keyPEM blobs and attaches
them as tls.Config.Certificates when both are supplied; supplying only
one is rejected. tlsConfigFor reads the leaf certificate and key from
the same cert-manager Secret (the tls.crt / tls.key entries populated
by the issued Certificate, which carries serverAuth and clientAuth
EKUs) when spec.tls.authClients is yes, and fails fast with a
descriptive error if either entry is missing or empty.

Unit tests cover the new branches: client cert loaded, mismatched
cert/key arguments, and malformed cert/key PEMs.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
MakeSlaveOfWithPort dialed the local Redis instance at
net.JoinHostPort(ip, masterPort) and sent SLAVEOF masterIP masterPort,
collapsing two distinct ports into one. In same-cluster failover the
two are equal so the bug was dormant, but SetExternalMasterOnAll
threads the external master's port through this API: when that port
differs from the local Redis port the operator dials the local pod on
the wrong port and the slave-of reconfig fails for every external
master takeover.

The function now takes localPort and masterPort as independent
arguments. The dial uses ip:localPort, the SLAVEOF command keeps
masterIP:masterPort. SetExternalMasterOnAll resolves localPort from
spec.Redis.Port before iterating pods; the same-cluster callers
(SetOldestAsMaster, SetMasterOnAll) pass the same port for both values
since their masterPort always equals localPort.

Ported from a downstream fork of spotahome/redis-operator. Adapted:
the interface line also carries the masterName parameters this repo
threads through GetSentinelMonitor and SetCustomSentinelConfig.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The early (nil, nil) return on len(caPEM) == 0 also accepted a
non-empty certPEM/keyPEM pair, which would silently disable TLS in a
misconfigured mTLS path, the same silent plaintext fallback already
guarded against on the caller side. Fail fast in BuildTLSConfig
instead.

New test TestBuildTLSConfigRejectsClientMaterialWithoutCA covers both
cert+key without CA and cert-only without CA.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
In-pod self-dials hit hosts that were not in the cert SAN list,
breaking startup whenever the verifying party (redis-cli >= 6,
oliver006/redis_exporter, the Sentinel monitor target) actually checks
the certificate against the dialed host.

Three call sites were affected:

- The liveness probe dialed `$(hostname)`, the bare StatefulSet pod
  name (e.g. `rfr-myrf-0`). The wildcard `*.<headless>` SAN does not
  match a bare label: X.509 wildcards consume exactly one label.
- redis_exporter and sentinel-exporter dialed `127.0.0.1`, and the
  Sentinel monitor config statement did the same. 127.0.0.1 was not in
  the cert's IPAddresses field, only DNS SANs were emitted.

Fix:

- redisCertificateSANs (renamed from redisCertificateDNSNames) now
  returns both DNSNames and IPAddresses. localhost joins the DNS set
  so the redis-cli liveness probe verifies cleanly; 127.0.0.1 and ::1
  join the IP set so the in-pod exporter and Sentinel monitor target
  verify against the same cert.
- The Redis and Sentinel liveness/readiness probes switch from
  `-h $(hostname)` to `-h localhost`, which the new DNS SAN covers.

Tests assert the new default IP SANs and the localhost DNS SAN, guard
against silent regression of the default IPAddresses slice, and check
that the probes dial localhost. The plain-text probe expectations move
to localhost too: the SAN-mismatch concern does not apply there, but
the codepath is shared.

Ported from a downstream fork of spotahome/redis-operator. Adapted:
the probes keep the engine-aware CLI binary and the configured master
name, and this repo has a DisableMyMaster probe fixture the fork did
not, updated to match.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The FQDN SAN entries hardcoded ".svc.cluster.local", so on a cluster
running with a non-default --cluster-domain (on-prem clusters routinely
use "cluster.internal" or a site-specific value) the cert-manager
Certificate produced by the operator did not cover the FQDNs clients
actually resolve, and TLS verification failed for every routed FQDN
dial.

Plumbing:

- New `--cluster-domain` CLI flag (default "cluster.local") on the
  operator binary.
- Threaded through the existing `redisfailover.Config` struct, the
  factory, and `NewRedisFailoverKubeClient`, which now takes the
  cluster domain as a constructor argument and stores it on the
  client.
- `generateRedisCertificate` and `redisCertificateSANs` accept the
  cluster domain and template the `.svc.<domain>` FQDN SAN entries
  accordingly. An empty value falls back to "cluster.local" so
  deployments that never set the flag keep working.

Tests: every NewRedisFailoverKubeClient caller passes the default
"cluster.local", and a new case asserts that with
--cluster-domain=cozy.local the FQDN SANs use ".cozy.local" and the
default ".cluster.local" form is not silently added alongside, which
would mask SAN-mismatch bugs.

Ported from a downstream fork of spotahome/redis-operator. Adapted:
this repo has additional test fixtures constructing the kube client,
all updated to the new signature.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The operator only reads the TLS secret (get) and publishes the CA-only
secret (create/update/patch); it never lists or watches secrets, so
granting list and watch widened cluster-wide secret read exposure for
nothing. Scope the ClusterRole to create/get/patch/update.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
A TLS secret without a populated ca.crt is an expected transient state
under asynchronous cert-manager issuance, and the operator already
treats it as a soft deferral. Log it at debug to match the sibling
"TLS secret not present yet" branch instead of warning on every
reconcile before the CA is ready.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The CA-only secret tests configured GetSecret and CreateOrUpdateSecret
expectations but never asserted them, so a missing interaction passed
silently. Add ms.AssertExpectations(t) to every test that sets
expectations.

Ported unchanged from a downstream fork of spotahome/redis-operator.

Signed-off-by: Arsolitt <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Replace the hand-written DeepCopy functions for TLSSettings,
CertManagerSettings and LocalSecretReference with the output of the
repository's own generator, so the file matches what update-codegen
produces: alphabetical placement and the certmanagerv1 import alias.
The copy semantics are unchanged.

Generated with kube::codegen::gen_helpers from k8s.io/code-generator
kubernetes-1.33.0, the version ghcr.io/slok/kube-code-generator:v0.7.0
bundles; that image has no arm64 build, so the generator was invoked
directly at the same version.

Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei
lexfrei marked this pull request as ready for review September 1, 2026 18:06
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants what the patched operator calls: Create,
  Get and Update on Secrets, so it can publish the CA-only Secret, and
  Create, Delete, Get and Update on cert-manager Certificates, for the
  spec.tls.certManager path this platform does not use. Every verb is one
  the patched source actually calls; neither resource is listed or
  watched, so neither carries list or watch.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

The RBAC grant is narrower than the ported operator's own chart asks for:
that one adds delete, list, patch and watch on Secrets and every verb on
Certificates. Neither is reachable from the source, so they are not
granted here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 1b23aa4b4 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and two upstream tests read the CRD YAML that stays behind, so
`go test ./...` on the patched tree fails in exactly those two; the
image build runs `go build` alone.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries two changes that are not TLS and take effect with
TLS off: the Redis and Sentinel probes dial -h localhost instead of
-h $(hostname), and MakeSlaveOfWithPort connects to the replica on its
own port rather than on the master's. Both come with the upstream PR as
ported, and the probe change alters the pod template, so the first
reconcile with this image rolls every existing Redis StatefulSet and
Sentinel Deployment once. The patch header names them, so a reader of patches/
does not take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 1, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 20580032e rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The probes dialled -h $(hostname). Under TLS the probe host was changed
to localhost, which is a SAN entry on the server certificate along with
the loopback addresses, while the bare pod hostname is not.

It was changed for every failover, including those that never enabled
TLS. The probe command is part of the pod template, so with that change
every existing RedisFailover's Redis StatefulSet and Sentinel Deployment
differ from their live spec the moment the new operator image starts, and
each one rolls its pods once — a restart of every Redis cluster under
management, caused by an operator upgrade rather than by any spec change
the user made. Plaintext therefore has to render byte-identically to what
the operator emitted before.

Pick the probe host from whether TLS is enabled for that failover:
localhost with TLS on, $(hostname) otherwise. Restore the plaintext
expectations in the existing probe tests and pin them, across custom
port, master name and engine, so an unconditional host cannot come back
unnoticed.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The two tests read the generated CRD from manifests/ and from the chart's
crds/ directory. Consumers that carry this operator as a source-only
patch take the Go tree and leave those directories out, so both tests
fail there for want of a fixture rather than for anything about the
operator, and `go test ./...` cannot be used to check the patched tree
at all.

Skip when none of the three committed copies is present, naming the
paths that were looked for. A tree carrying some copies but not the
others is a different case — they are written together and expected to
travel together — so a partial set stays a failure, and in a full
checkout both tests run and assert exactly as before.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The cert-manager secretName godoc named "<rfname>-tls" as the default
it overrides. The operator generates "rftls-<name>", so the name in the
doc matches nothing the operator ever creates. That godoc becomes the
CRD field description, so the wrong default also shipped into kubectl
explain on every cluster carrying the CRD; the three committed CRD
copies are regenerated with it.

The probeHost comment said the probes have to dial localhost because the
bare pod hostname is not a SAN entry. redis-cli verifies the certificate
chain and not the hostname, so the SAN list does not force the choice.
State what is true instead: localhost is dialled because it is a SAN
entry on the generated certificate, alongside the loopback addresses.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 7fe92e4e7 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 38b9e3732 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 38b9e3732 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 38b9e3732 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 38b9e3732 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Redis and Sentinel reject a client that presents no certificate while
tls-auth-clients is yes. tlsConfigFor loaded the operator's own
certificate only while spec.tls.authClients read yes, but that field is
mutable and the pods keep enforcing the value they booted with until
they roll. Flipping it to no therefore stopped the operator presenting
a certificate while the running pods still demanded one, so every check
and heal ran against its own instance and failed, until the pods
happened to restart for an unrelated reason.

Load the pair from the TLS Secret whenever it carries both tls.crt and
tls.key, independently of the spec. Offering a certificate to a server
that does not ask for one costs nothing; withholding it from a server
that does locks the operator out. A Secret carrying only one half still
yields no client certificate, so the dial reports what is missing
instead of every check failing early.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
spec.tls.authClients is mutable, but its only carrier is the generated
redis.conf and sentinel.conf. Redis reads that file once at startup,
the Redis StatefulSet rolls OnDelete and the roller keys on the
StatefulSet revision, and the healer pushes only
spec.redis.customConfig over CONFIG SET (heal.go:365). A change was
therefore admitted, rewrote the ConfigMaps, and then landed pod by pod
at whatever unrelated restart came next: a node drain, an OOM, or the
certificate renewal roll months later, with nothing in the release
history to point at as the cause.

Fold the effective directive into the content hashed for the
tls-secret-hash pod annotation, length-prefixed like the Secret keys
already are, so that "no" and "yes" cannot collide with a different
split of the same bytes. An unset value hashes as the "no" the API
defaulting fills in, so the two spellings of one server leave the pods
alone. Changing the value now changes the pod template, which publishes
a new StatefulSet updateRevision and rolls the Sentinel Deployment,
exactly as a renewed certificate does.

Unlike a tls.enabled flip, this roll cannot deadlock. The roller
refuses to delete any pod until every slave answers
(operator/redisfailover/checker.go:27), and that check dials with the
config built by tlsConfigFor
(operator/redisfailover/service/check.go:608, dialled at :614). The
operator now presents its client certificate
whatever the spec says, so pods still enforcing the old directive keep
answering it, and tls-auth-clients moves no listener: both sides of the
roll speak TLS on the same port. Old and new pods stay reachable for
the whole rollout.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
The const block lost its alignment when the TLS plumbing landed, and the
repo's lint gate does not report gofmt violations.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 38b9e3732 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 2, 2026
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.

Three pieces, and TLS is silently off unless all three are present:

- images/redis-operator/patches/tls.diff is the source half. The
  Dockerfile already applies patches/*.diff, and the shell expands that
  to labels.diff then tls.diff; they touch disjoint files, so the order
  does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
  it the apiserver prunes spec.tls from every RedisFailover the redis
  chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
  operator can publish the CA-only Secret named by
  spec.tls.caCertSecretName. Those are the only Secret calls in the
  patched source: upstream v3.3.5 only ever reads one, and the patch
  adds Create and Update on a Secret it addresses by name, so nothing
  lists, watches, patches or deletes.

tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.

tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.

cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.

The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.

tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.

The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lllamnyp added a commit to cozystack/community that referenced this pull request Sep 3, 2026
The engine table described `main` in the present tense, so it goes stale
every time the contract lands somewhere new. Anchor it to one dated
moment and drop "today" from the two column headers and the takeaway;
the table is a record of the gap this contract closes, not a tracker of
`main`.

Three claims were stale under that anchor. postgres, nats and qdrant all
render a TenantProjection sentinel on `main` and deliver
`<release>.tenant-ca`, so their "ca.crt to tenant?" cells and the
takeaway now say so, and section 6 and the rollout list no longer carry
nats and qdrant as convergence still to come.

Two identifiers were wrong outright: redis's CA Secret is
`<release>.ca-tls`, with a dot for the same collision-across-releases
reason section 4 gives for the canonical name, and the carried operator
patch is cut from freshworks-oss/redis-operator#85 commit 2ad2ed8d1.
mariadb's `<release>-ca-tls` is hyphenated and is left alone.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EoDWFXj3QE4Afg7Et9wFxy
Signed-off-by: Timofei Larkin <[email protected]>
lexfrei pushed a commit to cozystack/community that referenced this pull request Sep 3, 2026
The engine table described `main` in the present tense, so it goes stale
every time the contract lands somewhere new. Anchor it to one dated
moment and drop "today" from the two column headers and the takeaway;
the table is a record of the gap this contract closes, not a tracker of
`main`.

Three claims were stale under that anchor. postgres, nats and qdrant all
render a TenantProjection sentinel on `main` and deliver
`<release>.tenant-ca`, so their "ca.crt to tenant?" cells and the
takeaway now say so, and section 6 and the rollout list no longer carry
nats and qdrant as convergence still to come.

Two identifiers were wrong outright: redis's CA Secret is
`<release>.ca-tls`, with a dot for the same collision-across-releases
reason section 4 gives for the canonical name, and the carried operator
patch is cut from freshworks-oss/redis-operator#85 commit 2ad2ed8d1.
mariadb's `<release>-ca-tls` is hyphenated and is left alone.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei pushed a commit to cozystack/community that referenced this pull request Sep 3, 2026
The engine table described `main` in the present tense, so it goes stale
every time the contract lands somewhere new. Anchor it to one dated
moment and drop "today" from the two column headers and the takeaway;
the table is a record of the gap this contract closes, not a tracker of
`main`.

Three claims were stale under that anchor. postgres, nats and qdrant all
render a TenantProjection sentinel on `main` and deliver
`<release>.tenant-ca`, so their "ca.crt to tenant?" cells and the
takeaway now say so, and section 6 and the rollout list no longer carry
nats and qdrant as convergence still to come.

Two identifiers were wrong outright: redis's CA Secret is
`<release>.ca-tls`, with a dot for the same collision-across-releases
reason section 4 gives for the canonical name, and the carried operator
patch is cut from freshworks-oss/redis-operator#85 commit 2ad2ed8d1.
mariadb's `<release>-ca-tls` is hyphenated and is left alone.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei pushed a commit to cozystack/community that referenced this pull request Sep 4, 2026
The engine table described `main` in the present tense, so it goes stale
every time the contract lands somewhere new. Anchor it to one dated
moment and drop "today" from the two column headers and the takeaway;
the table is a record of the gap this contract closes, not a tracker of
`main`.

Three claims were stale under that anchor. postgres, nats and qdrant all
render a TenantProjection sentinel on `main` and deliver
`<release>.tenant-ca`, so their "ca.crt to tenant?" cells and the
takeaway now say so, and section 6 and the rollout list no longer carry
nats and qdrant as convergence still to come.

Two identifiers were wrong outright: redis's CA Secret is
`<release>.ca-tls`, with a dot for the same collision-across-releases
reason section 4 gives for the canonical name, and the carried operator
patch is cut from freshworks-oss/redis-operator#85 commit 2ad2ed8d1.
mariadb's `<release>-ca-tls` is hyphenated and is left alone.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 4, 2026
## What this PR does

`tls.enabled` puts Redis and Sentinel on a TLS-only listener with a
per-release cert-manager chain, and hands the tenant the CA certificate
as `redis-<name>.tenant-ca`. Off unless set.

redis-operator v3.3.5 has no TLS support. The work is ported upstream in
freshworks-oss/redis-operator#85, which is open and not in a release, so
this carries it as three patches over the v3.3.5 tarball.
`images/redis-operator/patches/tls.diff` is the source half, cut from
that PR's commit `2ad2ed8d1`, which the patch header pins; it is applied
by the glob the image Dockerfile already runs, after the Dockerfile
strips the tarball's `manifests`, `charts`, `example` and `.github`, so
the image builds from source alone and the upstream tests that read the
CRD YAML skip instead of failing against the stale v3.3.5 copy.
`patches/crd-tls.diff` adds `spec.tls` to the vendored CRD, and with it
the CEL rule that fixes `tls.enabled` at creation; without the schema
block the apiserver prunes the field on admission and the patched binary
waits for something it never sees, and the rule cannot exist without the
block it references. `patches/rbac-tls.diff` grants Create, Get and
Update on Secrets, so the operator can publish the CA-only Secret it
addresses by name. That is the whole delta, and it is narrower than the
ported operator's own chart asks for: those three are the only Secret
calls in the patched source, and cert-manager Certificates get no grant
at all, because the operator only touches one under
`spec.tls.certManager` and this chart pins `certificateSecret` mode
instead. One change in `tls.diff` is not TLS and takes effect with TLS
off, named in the patch header: `MakeSlaveOfWithPort` connects to the
replica on its own port rather than on the master's. It changes a dial,
not a pod template, so shipping the rebuilt image rolls nothing; the
plaintext pod templates stay byte-identical to v3.3.5.

The image build registration this branch used to carry (`make -C
packages/system/redis-operator image` in the root `build:` target)
landed on main with the Valkey service (#3380), so the rebuilt operator
image is produced by CI from main as well; the digest in `values.yaml`
is still refreshed by release preparation, not here.

The chart patches re-apply at the end of `update:`, the same way
reloader, grafana-operator, ingress-nginx, seaweedfs and kamaji carry
theirs. Otherwise `rm -rf charts` takes TLS back out on the next `make
update` and nothing fails. `make update` reproduces the committed tree
byte for byte.

TLS is chosen when the instance is created. The config gets `port 0` and
the listener moves to `tls-port`, so TLS replaces plaintext rather than
running beside it, and a running instance cannot change mode: a replica
restarted onto the TLS listener cannot replicate from a master still on
plaintext, and Sentinels already on TLS would fail over to a stale
replica. The RedisFailover schema in the carried CRD patch rejects a
change of `tls.enabled`, which on an existing instance surfaces as a
failed HelmRelease upgrade while the instance keeps serving what it did;
the README points at creating a new instance and migrating. That is also
why TLS is not inferred from `external`: an externally reachable
instance is the one most likely to have clients an upgrade must not
drop. Existing releases keep their plaintext listener and are not
restarted by the rebuilt operator image.

The tenant gets `ca.crt` and nothing else, through `tenantsecrets`. The
chart's own Secrets take a dot between the release and the suffix
(`<release>.ca-tls`, `<release>.tls`, `<release>.ca-cert`): the suffixes
are dot-free, so the split is unique and release `foo`'s CA Secret can
no longer be release `foo-ca`'s leaf, the collision the hyphenated form
had. The chart renders a `TenantProjection` pointing at
`<release>.ca-tls`, and the CA-extraction controller re-encodes the one
named key from the parsed certificate, so nothing that is not a
certificate gets through. Same path as postgres, mongodb, nats and
qdrant.

No Secret of this release is granted to the tenant by name beyond the
generated password, and that includes `<release>.ca-cert`, the CA-only
copy the operator publishes for itself. It holds no key today, but the
operator has Create and Update on Secrets cluster-wide, so what sits at
that name is the operator's call rather than the platform's, and RBAC
cannot filter by key. `hack/check-redis-rd-secrets.bats` pins the whole
rendered Secret surface instead of a list of known-bad names, because an
include with no `resourceNames` and no `matchLabels` restricts nothing.

The chainsaw suite for redis gains two cases. A TLS fixture asserts the
cert-manager chain is issued, the TLS-probed Redis replicas and Sentinel
come up, the operator publishes its CA-only Secret under the widened
ClusterRole, the tenant receives exactly `ca.crt` through
`tenantsecrets` while the key-bearing Secrets stay off that path, and a
`redis-cli --tls` PING through the service name completes with the same
CA. A second case applies `tls.enabled` to a running plaintext release
and asserts the refusal: the HelmRelease upgrade fails with the schema's
message, the StatefulSet keeps its revision with both replicas Ready,
plaintext still answers and no TLS listener appears.

Certificate renewal rolls the pods from inside the operator. Redis and
Sentinel read the certificate once at startup, so the patched operator
stamps a hash of the TLS Secret's `tls.crt` and `ca.crt` into the pod
template of both the Redis StatefulSet and the Sentinel Deployment on
every reconcile, as `redis-failover.freshworks.com/tls-secret-hash`. A
renewal changes the hash; the operator's own roller replaces the Redis
pods one at a time and the Deployment controller rolls Sentinel. README
names the fallback: if the roll cannot run, the deadline is the old
certificate's expiry, past which the operator's own TLS connections fail
along with the clients.

`tls.authClients` maps to `tls-auth-clients` and defaults to `no`. The
platform mints no client certificate for the tenant and the CA key is
out of reach, so `yes` is only usable when the client certificate comes
from whoever also controls the CA. Changing the value on a running
release restarts the Redis and Sentinel pods: Redis reads the directive
from its config file only at startup, so the operator folds the
effective value into the certificate hash that drives the rollout, and
the change lands when it is made rather than at the next unrelated
restart.

Follow-ups in the carried operator patch go upstream first, to
freshworks-oss/redis-operator#85 or a successor there, and are picked up
here afterwards; the carried patch does not grow Go that upstream has
not seen. `tlsConfigFor` fetches the Secret on every call, tens of GETs
per reconcile. The renewal roll is static analysis so far: the live
check, `cmctl renew` on a running release while watching the `rfr-*` and
`rfs-*` pods roll, is still owed and belongs with the e2e follow-up.

Two facts about the shipped image rather than the patch.
`packages/system/redis-operator/values.yaml` keeps the digest of the
image built before the patch until release preparation rebuilds it, so
on `main` between this merge and the next release-prep, `tls.enabled`
renders the chain and `spec.tls` while the running operator ignores them
and serves plaintext. And the pinned v3.3.5 tarball carries GO-2026-5026
in `golang.org/x/net` v0.53.0, reached through
`PodService.UpdatePodLabels`; the port neither introduces nor fixes it,
upstream `master` is already on v0.55.0, so it lands with the next tag
bump rather than here.

Outside the patch: `cozyvalues-gen` renders `{}` in the README Value
column for `tls.authClients`: a child property without a default
inherits the parent object's `{}`, and `make generate` reproduces it
byte for byte, so it is a generator bug rather than something to
hand-edit here.

### Screenshots

No UI changes.

### Downstream repositories

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [x]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up: cozystack/terraform-provider-cozystack#37
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

I walked the trigger map against the diff file by file.
terraform-provider-cozystack is reached, tracked as
cozystack/terraform-provider-cozystack#37 rather than a speculative PR:
`values.schema.json` gains a `tls` object, and the provider hand-writes
a schema, model and expand/flatten pair per Kind. It already models
`tls` for postgres, kafka, nats and qdrant, not for redis. Redis needs
different wording from those four, whose descriptions say to omit the
block to follow `external`, which is the one thing redis does not do.

Nothing else is reached. The website regenerates this package's
reference page from `README.md` on a stable tag, so the README changes
need no PR there. No package added, renamed or removed.
`packages/core/platform/values.yaml`, variant and bundle composition and
release asset names are untouched, and so are `hack/package.mk`,
`hack/common-envs.mk` and `hack/update-crd.sh`, which is what ccp and
external-apps-example read. The two new files under `hack/` are bats
unit tests. No node prerequisite, installer value, telemetry metric or
cozy-proxy label changes.

### Release note

```release-note
feat(redis): add opt-in TLS for Redis and Sentinel. Set `tls.enabled: true` to serve both on a TLS-only listener backed by a per-release cert-manager chain; the tenant reads the CA certificate as the `<release>.tenant-ca` tenant secret. TLS is off unless set and is not inferred from `external`, so existing releases keep serving plaintext and are not restarted by the rebuilt operator image. TLS is chosen at creation: a running instance cannot switch modes, the change is rejected and surfaces as a failed HelmRelease upgrade, so add TLS by creating a new instance and migrating to it. Requires cert-manager, which the platform's `system` bundle installs on every variant. A renewed certificate rolls the Redis and Sentinel pods through the operator. TLS support is carried as a downstream patch over redis-operator v3.3.5 until freshworks-oss/redis-operator#85 lands in a release.
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants