Repository navigation
feat(tls): cert-manager-managed TLS for RedisFailover - #1
Conversation
- Go: 1.20 -> 1.23.0 (toolchain 1.23.4)
- kooper: 2.4.0 -> 2.7.0 (resolves incompatibility with client-go
leaderelection API: ConfigMapsLeasesResourceLock was removed)
- k8s.io/{api,apimachinery,client-go,apiextensions-apiserver}:
0.27.3 -> 0.31.0 (pulled transitively by kooper)
- prometheus/client_golang: 1.16.0 -> 1.19.1
- stretchr/testify: 1.8.4 -> 1.9.0
Adjust PVC test fixtures to use VolumeResourceRequirements which
replaced ResourceRequirements as the type of
PersistentVolumeClaimSpec.Resources in k8s 1.31.
Lays the groundwork for adding cert-manager API as a dependency
for native TLS support.
Signed-off-by: Arsolitt <[email protected]>
Replace github.com/go-redis/redis/v8 with github.com/redis/go-redis/v9 (v9.19.0). The v8 module is no longer receiving updates. No code changes required beyond the import path: every API used by service/redis/client.go (NewClient, Options, NewSentinelClient, NewBoolCmd, NewIntCmd, NewStatusCmd, NewSliceCmd, Process, Info, SlaveOf, CkQuorum, ConfigSet) is preserved between v8 and v9. The v9 breaking changes (Pipeline thread-safety, hook interface, *redis.Z, MaxConnAge, IdleTimeout, WithContext, Pipeline.Close) are not used by the operator. Required for native TLS support: v9 receives ongoing maintenance and the tls.Config option remains available on Options. Signed-off-by: Arsolitt <[email protected]>
Adopt vendor-neutral naming for container build files (Dockerfile -> Containerfile). - docker/app: bump golang:1.20-alpine -> golang:1.23-alpine3.20, pin alpine:latest -> alpine:3.20 - docker/development: bump golang:1.20-alpine -> golang:1.23-alpine3.20 (code-generator and mockery versions left untouched; refresh is a separate concern) - Makefile: point image build targets at the renamed files - CI workflow: - fix push trigger to use the actual default branch (master, not main) - bump actions/checkout@v3 -> @v4, actions/setup-go@v4 -> @v5, golangci-lint-action@v3 -> @v6 (version v1.61 supports Go 1.23), medyagh/[email protected] -> @v0.0.18, azure/setup-helm@v3 -> @v4 (helm v3.15.4) - bump Kubernetes test matrix to 1.29/1.30/1.31 to match the k8s client-go version pulled in by the previous deps bump - expand short flags (--yes, --filename) per project convention Signed-off-by: Arsolitt <[email protected]>
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.IssuerReference 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.15.5 (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) DeepCopy methods for TLSSettings, CertManagerSettings and LocalSecretReference are added by hand (the project's existing zz_generated file is hand-maintained rather than regenerated each commit); the cert-manager imports needed for these methods are also wired into the existing zz_generated file. 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. Signed-off-by: Arsolitt <[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.
Signed-off-by: Arsolitt <[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 are extended by hand with the new methods (the project's mocks/ tree is hand-maintained between mockery runs). 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 internal redis client still uses plaintext; TLS in service/redis/client.go lands in the next commit. Signed-off-by: Arsolitt <[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. Signed-off-by: Arsolitt <[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. Signed-off-by: Arsolitt <[email protected]>
Bump to the current upstream stable versions:
- Go directive: 1.26.0 (was 1.23). The previous CODEGEN_IMAGE could
not parse three-digit minor versions, which broke generate-crd.
- k8s.io/{api,apimachinery,client-go,apiextensions-apiserver}:
0.31.0 -> 0.36.1
- kooper: 2.7.0 -> 2.10.0
- cert-manager and prometheus client_golang were already at latest
via earlier transitive bumps
Update the codegen tooling:
- CODEGEN_IMAGE: slok/kube-code-generator v1.27.0 -> v0.9.0. The
upstream project rewrote its CLI in the v0 line (drop update-*.sh
scripts, single Go binary configured with flags), so the Makefile
targets are rewritten to call it directly via flags rather than
through env-vars + a bash wrapper. The new image's bundled Go is
older than our go.mod directive, so GOTOOLCHAIN=auto is set on the
docker run to let it download a matching toolchain on demand.
Regenerate the CRD against the new TLSSettings types so installs
through helm/kubectl pick up the new spec.tls fields out of the box.
The chart's crds/ copy and the kustomize base copy are also updated
by the same generate-crd target.
Drive-by: fix non-constant format string in GetNumberMasters which
go vet started flagging under newer toolchains.
Signed-off-by: Arsolitt <[email protected]>
Adds an env: [] knob to charts/redisoperator/values.yaml and renders the corresponding env block in the Deployment when set. Documents KUBE_FEATURE_WatchListClient=false as the workaround when running against API servers that do not deliver BOOKMARK events on the initial list — without it the controller hangs forever in "Waiting for caches to sync". Signed-off-by: Arsolitt <[email protected]>
Aligns the operator image with the go 1.26 toolchain required by go.mod and refreshes the alpine base in both the build and runtime stages. Earlier golang:1.23-alpine3.20 / alpine:3.20 stack could no longer compile after the dependency bump to k8s 0.36 / cert-manager 1.20. Signed-off-by: Arsolitt <[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. Includes regenerated deepcopy (now produced by controller-gen, which also reformats prior entries) and the regenerated CRD manifest in all three published locations: chart crds, kustomize base, and manifests root. The go directive moves from 1.26.0 to 1.26.3 as a side-effect of running the generator under that toolchain. Signed-off-by: Arsolitt <[email protected]>
slok/kube-code-generator v0.9.0 dropped the explicit --client-package flag; the generator now infers the client package from --go-gen-out combined with the go.mod module path. Passing the removed flag caused update-codegen to fail with "unknown long flag '--client-package'". Signed-off-by: Arsolitt <[email protected]>
Re-runs make update-codegen and make generate-crd against the canonical
toolchain after the Makefile fix. The diff captures three shifts:
- deepcopy: refreshed by deepcopy-gen, replacing the temporary
controller-gen output that landed alongside the extraSANs change.
Format and import aliases match the upstream style again.
- clientset: regenerated; the public surface contracts in Get/List/
Watch/Create/Update/Patch are unchanged, but the typed fake clients
shrunk to the generic client-go testing wrappers and a stale doc.go
is dropped.
- informers / listers: newly generated tree under client/k8s. Not
consumed by the operator itself yet, but kept versioned to keep
downstream consumers and future controller refactors free of an
extra generation step.
- CRD manifest: regenerated and synced to manifests/, kustomize base
and the chart crds directory.
Signed-off-by: Arsolitt <[email protected]>
Apply goimports with -local=github.com/spotahome/redis-operator and the gofmt struct-literal alignment that golangci-lint v2 enforces by default. Pure formatting — no behaviour or API changes. The realignments in service/redis/client.go are a follow-up to the TLS plumbing change (62ac27b): adding TLSConfig to the existing rediscli options literals shifted the longest field name, which gofmt then expects all sibling colons to track. Signed-off-by: Arsolitt <[email protected]>
golangci-lint v2 reorganised its CLI and configuration schema. Inline flags like '-E goimports --timeout 3m' no longer work, and v2's default 'standard' linter set bundles many staticcheck families (QF/ST/S1/SA1019) that the previous v1.61 default excluded. Out-of-the-box this would surface a flood of issues in upstream code that pre-dates this fork and is unrelated to the work happening here. Lock the staticcheck check list down to the SA* bug-class families (minus SA1019, since the kooper-style SlaveOf usage in service/redis/client.go is deliberate) so the noise level matches what v1.61 produced. Move the in-repo errcheck exception for upstream's deferred rClient.Close() calls into the same config. Goimports becomes a v2 'formatter' rather than a '-E' flag, configured with the project's local prefix. scripts/check.sh stops passing v1-only flags and just delegates to the new config. Signed-off-by: Arsolitt <[email protected]>
The fork now builds against Go 1.26 and client-go 0.36, which makes the
CI pipeline that shipped with the upstream repository unusable:
* golangci-lint v1.61 predates Go 1.26 and rejects the toolchain. Bump
to v2.12.2 with the matching golangci/golangci-lint-action@v9 (v2
requires the action's major version 9 or newer).
* The integration-test matrix targeted Kubernetes 1.29/1.30/1.31. With
client-go 0.36, the WatchListClient feature gate is enabled by
default, and against API servers older than 1.36 the initial events
stream never delivers the BOOKMARK event the client waits for —
informers hang on cache sync until the job times out. Bump the
matrix to 1.34.8 / 1.35.5 / 1.36.1 (current supported releases) and
pass KUBE_FEATURE_WatchListClient=false to the integration-test step
so older API servers in the matrix still pass.
* Refresh setup-minikube, setup-helm, setup-go, and actions/checkout
to their current major versions; bump the helm CLI used by the
chart-test job to v3.21.0.
Release Charts workflow gets the same setup-helm/checkout/helm CLI
bumps plus chart-releaser-action v1.7.0 so it keeps releasing chart
artefacts after the toolchain upgrade.
Signed-off-by: Arsolitt <[email protected]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 Walkthrough<review_stack_artifact> </review_stack_artifact> WalkthroughAdds native TLS support and cert-manager integration: CRD TLS fields and validation, cert-manager Certificate generation and service, Redis client TLS, operator wiring, generated clients/informers, chart and CI updates, and extensive TLS tests. ChangesTLS Feature Implementation
Build, Charts, CI, and Misc
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request introduces TLS support for the Redis Failover operator, allowing for encrypted communication between Redis, Sentinel, and the operator itself. It integrates with cert-manager for automated certificate provisioning while also supporting user-provided secrets. The changes include updates to the CRD API, configuration templates for Redis and Sentinel, and the internal Redis client to handle TLS handshakes. Critical feedback was provided regarding the lack of mutual TLS (mTLS) support in the operator's internal client; specifically, the operator needs to present its own client certificate when tls-auth-clients is enabled. Additionally, several Sentinel-specific TLS configuration directives were identified as missing or incorrectly formatted in the generation templates.
| caPEM, ok := secret.Data["ca.crt"] | ||
| if !ok { | ||
| return nil, fmt.Errorf("tls: secret %s/%s does not contain ca.crt", rf.Namespace, secretName) | ||
| } |
There was a problem hiding this comment.
The operator only extracts the CA certificate from the Secret. If spec.tls.authClients is set to yes (mutual TLS), the operator's internal Redis client will fail to authenticate because it is not presenting a client certificate. The operator should load tls.crt and tls.key from the same Secret and use them in the tls.Config.
There was a problem hiding this comment.
Addressed in 6827d2d. tlsConfigFor now reads tls.crt and tls.key from the same Secret when spec.tls.authClients is yes, and the operator presents them as a client certificate via tls.Config.Certificates. It fails fast with a descriptive error if either entry is missing or empty in mutual-TLS mode.
| // | ||
| // Returns (nil, nil) when caPEM is empty: callers treat a nil config as | ||
| // "no TLS" and dial in plaintext. | ||
| func BuildTLSConfig(caPEM []byte, serverName string) (*tls.Config, error) { |
There was a problem hiding this comment.
The BuildTLSConfig function should be updated to accept client certificate and key PEM data. This is necessary to support mutual TLS (mTLS) when tls-auth-clients is enabled on the Redis/Sentinel side, allowing the operator to authenticate itself.
| func BuildTLSConfig(caPEM []byte, serverName string) (*tls.Config, error) { | |
| func BuildTLSConfig(caPEM, certPEM, keyPEM []byte, serverName string) (*tls.Config, error) { |
There was a problem hiding this comment.
Addressed in 6827d2d. BuildTLSConfig now accepts optional certPEM and keyPEM PEM blobs; when both are supplied it attaches the resulting tls.Certificate to tls.Config.Certificates. Supplying only one of the two is rejected with an explicit error so a call site cannot silently drop the client cert.
| return &tls.Config{ | ||
| RootCAs: pool, | ||
| ServerName: serverName, | ||
| MinVersion: tls.VersionTLS12, | ||
| }, nil |
There was a problem hiding this comment.
Load the client certificate into the tls.Config if provided. This ensures the operator can connect when the cluster requires client certificate validation.
config := &tls.Config{
RootCAs: pool,
ServerName: serverName,
MinVersion: tls.VersionTLS12,
}
if len(certPEM) > 0 && len(keyPEM) > 0 {
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("tls: failed to load client certificate: %w", err)
}
config.Certificates = []tls.Certificate{cert}
}
return config, nilThere was a problem hiding this comment.
Addressed in 6827d2d. BuildTLSConfig builds the tls.Certificate via tls.X509KeyPair after constructing the base tls.Config and returns a wrapped error if the PEMs are malformed. Unit tests cover the happy path, the half-supplied (cert without key / key without cert) error cases, and a malformed-PEM case.
| tls-key-file ` + tlsKeyFile + ` | ||
| tls-ca-cert-file ` + tlsCAFile + ` | ||
| tls-auth-clients {{ .Spec.TLS.AuthClients }} | ||
| tls-replication yes |
There was a problem hiding this comment.
There was a problem hiding this comment.
This directive does not exist in Redis Sentinel. The official sentinel.conf template uses tls-replication yes at the top level (no sentinel prefix) to enable TLS for outbound replication-protocol connections, which is what the operator emits. The current configuration was verified end-to-end against a live cluster: Sentinel monitors the master over TLS, the slave replicates over TLS, and the plaintext port refuses connections. Prefixing the directive with sentinel would make redis-sentinel reject the config at startup.
| tls-key-file ` + tlsKeyFile + ` | ||
| tls-ca-cert-file ` + tlsCAFile + ` | ||
| tls-auth-clients {{ .Spec.TLS.AuthClients }} | ||
| tls-replication yes |
There was a problem hiding this comment.
When TLS is enabled, Sentinel-to-Sentinel communication should also be encrypted. You are missing the sentinel sentinel-tls-replication yes directive in the template, which is required for Sentinels to communicate with each other over TLS.
| tls-replication yes | |
| sentinel tls-replication yes | |
| sentinel sentinel-tls-replication yes |
There was a problem hiding this comment.
sentinel sentinel-tls-replication yes is not a real Redis directive. Sentinel-to-Sentinel TLS communication uses the same tls-port, tls-cert-file, tls-key-file and tls-ca-cert-file directives that are already emitted in the sentinel.conf template — each sentinel listens on its TLS port and the others dial it through that port. The live-cluster verification covered three sentinels reaching quorum over TLS without any additional directive.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yaml:
- Around line 9-23: Pin third-party actions to immutable commit SHAs, add a
minimal workflow permissions block, and disable checkout credential persistence:
replace tag-based usages (actions/checkout@v6, actions/setup-go@v6,
golangci/golangci-lint-action@v9 and the other usages
medyagh/[email protected], azure/setup-helm@v5) with their verified commit
SHAs, add a top-level permissions: block restricting GITHUB_TOKEN to least
privilege needed (for example permissions: contents: read) and set
persist-credentials: false on every actions/checkout step (the steps using
actions/checkout in the job) so credentials are not left in the workspace.
In @.github/workflows/helm.yml:
- Around line 12-14: The workflow is using tag-based action refs
(actions/checkout@v6, azure/setup-helm@v5, helm/[email protected])
and missing persist-credentials on checkout; update those action refs to
specific commit SHAs (replace the tag refs with the corresponding commit SHA
pinned refs) and add persist-credentials: false to the actions/checkout step
(alongside fetch-depth: 0) to disable persisted checkout credentials and harden
the token usage.
In `@charts/redisoperator/templates/service-account.yaml`:
- Around line 102-117: The RBAC rule granting create/delete/update/patch/watch
on cert-manager.io certificates should be guarded by a Helm value to avoid
unnecessary privileges; update the service-account template that currently lists
apiGroups: [cert-manager.io] and resources: [certificates] so it is wrapped in a
conditional (e.g. check .Values.rbac.certManager.enabled) and only renders those
verbs and the cert-manager rule when the value is true, leaving other RBAC rules
untouched and defaulting the new value to false in values.yaml.
In `@docker/development/Containerfile`:
- Line 1: The Containerfile currently pins the base image as "FROM
golang:1.23-alpine3.20" which is out of sync with go.mod and the app
Containerfile; update that FROM value to the repo's bumped Go toolchain (e.g.,
"golang:1.26.3-alpine3.23") so the development image matches go.mod and
docker/app/Containerfile, then rebuild/test to ensure tooling/lint/codegen
behavior is aligned.
In `@operator/redisfailover/service/check.go`:
- Around line 31-39: The current tlsConfigFor logic accepts a present but empty
ca.crt and lets redis.BuildTLSConfig return (nil, nil), causing callers to fall
back to plaintext; update tlsConfigFor (the block using s.GetSecret, caPEM,
redis.BuildTLSConfig and GetRedisName) to check len(caPEM) == 0 after retrieving
secret.Data["ca.crt"] and return a descriptive error (e.g., "tls: secret
<namespace>/<secretName> contains empty ca.crt") instead of calling
redis.BuildTLSConfig when the CA is empty so we fail fast and avoid silent
plaintext dialing.
- Around line 51-52: The CheckSentinelMonitor function exposes a variadic
monitor parameter but still indexes monitor[0], which can panic when no
arguments are passed; update the API by either making the first monitor a
required parameter (change signature from CheckSentinelMonitor(rFailover
*redisfailoverv1.RedisFailover, sentinel string, monitor ...string) to accept
monitor string) or add an explicit guard at the start of CheckSentinelMonitor
that returns a clear error when len(monitor) == 0; apply the same fix to the
other occurrence of CheckSentinelMonitor usage/definition found in the file so
no path can call it without a monitor and cause a panic.
In `@operator/redisfailover/service/generator.go`:
- Around line 1050-1052: The decorated init/extra containers created by
getInitContainersWithRedisEnv and getExtraContainersWithRedisEnv currently only
receive REDIS_ADDR but not TLS volume mounts or certificate env vars, so when
TLSEnabled(rf) is true you must mirror the exporter handling: add
tlsVolumeMount() to their VolumeMounts and inject the same cert/CA env vars (the
ones used for the exporter) into their Env lists; update the code paths in
getInitContainersWithRedisEnv and getExtraContainersWithRedisEnv to check
TLSEnabled(rf) and append tlsVolumeMount() plus the cert-related env entries so
sidecars/init containers can use rediss:// properly (also apply the same fix in
the other similar block around the 1323-1330 region).
In `@service/redis/client.go`:
- Around line 281-290: MakeSlaveOfWithPort is dialing net.JoinHostPort(ip,
masterPort) which incorrectly uses the upstream master's port for the local
connection; change the dial address to use the local Redis port (e.g.,
net.JoinHostPort(ip, redisPort)) while still sending the SLAVEOF command with
masterIP and masterPort so the local client connects to the local instance but
configures it to follow the external master. Update the creation of
rediscli.Options Addr in MakeSlaveOfWithPort (and ensure MakeSlaveOf still
delegates correctly) so only the SLAVEOF uses masterPort.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9ad5b95e-93ef-47bd-8499-47c33114960c
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (67)
.github/workflows/ci.yaml.github/workflows/helm.yml.golangci.ymlMakefileapi/redisfailover/v1/defaults.goapi/redisfailover/v1/types.goapi/redisfailover/v1/validate.goapi/redisfailover/v1/validate_tls_test.goapi/redisfailover/v1/zz_generated.deepcopy.gocharts/redisoperator/crds/databases.spotahome.com_redisfailovers.yamlcharts/redisoperator/templates/deployment.yamlcharts/redisoperator/templates/service-account.yamlcharts/redisoperator/values.yamlclient/k8s/clientset/versioned/clientset.goclient/k8s/clientset/versioned/doc.goclient/k8s/clientset/versioned/fake/clientset_generated.goclient/k8s/clientset/versioned/typed/redisfailover/v1/fake/fake_redisfailover.goclient/k8s/clientset/versioned/typed/redisfailover/v1/fake/fake_redisfailover_client.goclient/k8s/clientset/versioned/typed/redisfailover/v1/redisfailover.goclient/k8s/clientset/versioned/typed/redisfailover/v1/redisfailover_client.goclient/k8s/informers/externalversions/factory.goclient/k8s/informers/externalversions/generic.goclient/k8s/informers/externalversions/internalinterfaces/factory_interfaces.goclient/k8s/informers/externalversions/redisfailover/interface.goclient/k8s/informers/externalversions/redisfailover/v1/interface.goclient/k8s/informers/externalversions/redisfailover/v1/redisfailover.goclient/k8s/listers/redisfailover/v1/expansion_generated.goclient/k8s/listers/redisfailover/v1/redisfailover.gocmd/redisoperator/main.gocmd/utils/flags.gocmd/utils/k8s.godocker/app/Containerfiledocker/development/Containerfileexample/redisfailover/tls.yamlgo.modmanifests/databases.spotahome.com_redisfailovers.yamlmanifests/kustomize/base/databases.spotahome.com_redisfailovers.yamlmetrics/metrics.gomocks/operator/redisfailover/service/RedisFailoverCheck.gomocks/operator/redisfailover/service/RedisFailoverClient.gomocks/operator/redisfailover/service/RedisFailoverHeal.gomocks/service/k8s/Services.gomocks/service/redis/Client.gooperator/redisfailover/checker.gooperator/redisfailover/checker_test.gooperator/redisfailover/ensurer.gooperator/redisfailover/ensurer_test.gooperator/redisfailover/service/check.gooperator/redisfailover/service/check_test.gooperator/redisfailover/service/client.gooperator/redisfailover/service/constants.gooperator/redisfailover/service/generator.gooperator/redisfailover/service/generator_test.gooperator/redisfailover/service/generator_tls_test.gooperator/redisfailover/service/heal.gooperator/redisfailover/service/heal_test.gooperator/redisfailover/service/names.goscripts/check.shservice/k8s/certificate.goservice/k8s/k8s.goservice/k8s/secret.goservice/k8s/secret_test.goservice/k8s/statefulset_test.goservice/k8s/util.goservice/redis/client.goservice/redis/tls.goservice/redis/tls_test.go
💤 Files with no reviewable changes (1)
- client/k8s/clientset/versioned/doc.go
| CheckSentinelMonitor(rFailover *redisfailoverv1.RedisFailover, sentinel string, monitor ...string) error | ||
| GetMasterIP(rFailover *redisfailoverv1.RedisFailover) (string, error) |
There was a problem hiding this comment.
Don’t keep a variadic API that panics on empty input.
CheckSentinelMonitor() still does monitor[0] unconditionally, so the public interface can panic if a caller omits the variadic arg. Either make the first monitor value required in the signature or return an error when len(monitor) == 0.
Also applies to: 293-297
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@operator/redisfailover/service/check.go` around lines 51 - 52, The
CheckSentinelMonitor function exposes a variadic monitor parameter but still
indexes monitor[0], which can panic when no arguments are passed; update the API
by either making the first monitor a required parameter (change signature from
CheckSentinelMonitor(rFailover *redisfailoverv1.RedisFailover, sentinel string,
monitor ...string) to accept monitor string) or add an explicit guard at the
start of CheckSentinelMonitor that returns a clear error when len(monitor) == 0;
apply the same fix to the other occurrence of CheckSentinelMonitor
usage/definition found in the file so no path can call it without a monitor and
cause a panic.
There was a problem hiding this comment.
This is pre-existing behaviour in the upstream Spotahome operator, not a change introduced by this PR — the variadic signature predates the TLS work. Reshaping the interface (either making the first monitor argument required or adding an explicit guard) would touch every caller of CheckSentinelMonitor, which is outside the scope of a TLS feature PR. Worth filing as a separate cleanup issue upstream.
There was a problem hiding this comment.
@Arsolitt, that's a fair point — the panic risk pre-dates this PR and fixing it properly would require reshaping every call site, which is clearly out of scope here. Would you like me to open a GitHub issue to track this cleanup so it doesn't get lost?
There was a problem hiding this comment.
Thanks for offering. The pre-existing variadic panic is fairly easy to spot from a search of the upstream repository, and reshaping it touches every caller across both the upstream Spotahome project and this fork — better tracked there directly than as a downstream-fork issue that would always lag the upstream interface. I'll keep the note in this thread as the record. No issue needed.
There was a problem hiding this comment.
@Arsolitt, understood — tracking it upstream makes much more sense given the interface is shared across the Spotahome project and all its forks. The thread note here is a reasonable record. Thanks for the context!
(´・ω・`) 🐇
| if TLSEnabled(rf) { | ||
| volumeMounts = append(volumeMounts, tlsVolumeMount()) | ||
| } |
There was a problem hiding this comment.
Propagate TLS material to the containers that inherit REDIS_ADDR.
getInitContainersWithRedisEnv() / getExtraContainersWithRedisEnv() only append env vars, so with this change those containers can now receive REDIS_ADDR=rediss://... without getting tlsVolumeMount() or the cert/CA env needed to use it. Any sidecar or init container that already relies on the injected Redis env will break as soon as TLS is enabled. Mirror the exporter path for those decorated containers when TLSEnabled(rf) is true.
Also applies to: 1323-1330
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@operator/redisfailover/service/generator.go` around lines 1050 - 1052, The
decorated init/extra containers created by getInitContainersWithRedisEnv and
getExtraContainersWithRedisEnv currently only receive REDIS_ADDR but not TLS
volume mounts or certificate env vars, so when TLSEnabled(rf) is true you must
mirror the exporter handling: add tlsVolumeMount() to their VolumeMounts and
inject the same cert/CA env vars (the ones used for the exporter) into their Env
lists; update the code paths in getInitContainersWithRedisEnv and
getExtraContainersWithRedisEnv to check TLSEnabled(rf) and append
tlsVolumeMount() plus the cert-related env entries so sidecars/init containers
can use rediss:// properly (also apply the same fix in the other similar block
around the 1323-1330 region).
There was a problem hiding this comment.
Init and extra containers are an extension point the user owns. The operator already wires REDIS_ADDR (scheme adjusted to rediss:// when TLS is on), but the certificate material and any extra mount points are the user's responsibility — sidecars commonly bring their own auth mechanism (for example a redis-exporter image baked with its own CA bundle, or a sidecar that does not talk to Redis at all). Forcing a tlsVolumeMount() into every user-supplied container would also break containers that already mount their own /tls.
There was a problem hiding this comment.
@Arsolitt, that's a clear and well-reasoned design boundary — user-supplied init/extra containers are intentionally opaque to the operator, and injecting tlsVolumeMount() unconditionally would conflict with containers that already manage their own /tls mount or that don't communicate with Redis at all. The rediss:// scheme in REDIS_ADDR is enough signal for containers that do care about TLS to act on. Understood, I'll drop the suggestion.
✏️ Learnings added
Learnt from: Arsolitt
Repo: cozystack/redis-operator PR: 1
File: operator/redisfailover/service/generator.go:1050-1052
Timestamp: 2026-05-25T09:17:24.839Z
Learning: In `operator/redisfailover/service/generator.go` (redis-operator project, Go), `getInitContainersWithRedisEnv` and `getExtraContainersWithRedisEnv` intentionally do NOT inject `tlsVolumeMount()` or TLS cert env vars (e.g. `REDIS_EXPORTER_TLS_*`) into user-supplied init/extra containers, even when TLS is enabled. These containers are a user-owned extension point; the operator only adjusts `REDIS_ADDR` to use the `rediss://` scheme. Forcing TLS volume mounts would break sidecars that bring their own CA bundles, don't talk to Redis, or already mount `/tls` themselves. Do not flag this as a missing TLS propagation issue in future reviews.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Address review feedback from coderabbitai on operator/redisfailover/service/check.go:39: tlsConfigFor only verified that the ca.crt key existed in the TLS Secret, not that its value was non-empty. When the key was 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. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from gemini-code-assist on service/redis/tls.go:22, service/redis/tls.go:37, and operator/redisfailover/service/check.go:38: the operator only loaded the CA bundle from the TLS Secret, so its internal Redis client could connect to a server that ran in non-mutual-TLS mode (tls-auth-clients: no) but had no way to satisfy servers that ran in mutual mode — the handshake would be rejected with no client certificate to validate. BuildTLSConfig now takes optional certPEM/keyPEM PEM 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 already 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. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from coderabbitai on docker/development/Containerfile:1: the dev image still pinned golang:1.23-alpine3.20 while go.mod targets go 1.26.3 and docker/app/Containerfile already used golang:1.26.3-alpine3.23. Match the dev image so local builds, codegen and lint share the same toolchain as the production image. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from coderabbitai on .github/workflows/ci.yaml:23 and .github/workflows/helm.yml:14: - Pin every third-party action to an immutable commit SHA (with the resolved version pinned in a trailing comment for human readability). Tag references are mutable and let a compromised action push arbitrary code into the workflow on the next run. - Add a top-level permissions block to each workflow. CI runs with the minimum contents: read; the Release Charts workflow needs contents: write because chart-releaser-action pushes tags, releases and the gh-pages branch. Both replace GitHub's default broad GITHUB_TOKEN scope. - Set persist-credentials: false on every actions/checkout step so the short-lived GITHUB_TOKEN is not left behind in .git/config for later steps to abuse. Signed-off-by: Arsolitt <[email protected]>
Update test/integration/redisfailover/creation_test.go call sites that the TLS work changed but did not migrate: - CreateKubernetesClients now returns the cert-manager client too; destructure the 5th return into cmClient. - k8s.New takes that cert-manager client as an extra argument. - IsMaster and GetSentinelMonitor now require a *tls.Config; pass nil since the integration suite still talks to Redis in plaintext. Restores `go test -tags=integration` compile so the integration matrix turns green again across all three Kubernetes versions. Signed-off-by: Arsolitt <[email protected]>
CreateOrUpdateService used to overwrite the live Service's metadata with whatever the operator generated, dropping any labels and annotations that other actors (admission webhooks, service meshes, cluster operators stamping metadata) had attached. Merge the stored metadata with the operator-managed metadata before issuing the update so external annotations survive every reconcile. This is the patch we have been carrying as an out-of-tree fix on in-cluster builds; folding it into the fork so the same image works without a sidecar patch step. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from coderabbitai on service/redis/client.go:290: 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. This supersedes the original WONTFIX on PR #1 review thread 12 — the fix turned out cheap enough to fold into the TLS branch rather than defer. Signed-off-by: Arsolitt <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
service/redis/tls_test.go (1)
16-24: ⚡ Quick winAdd a regression case for client keypair + empty CA.
Given the new mTLS parameters, add coverage for
certPEM/keyPEMpresent with emptycaPEMto ensure this path errors instead of falling back.🧪 Proposed test addition
func TestBuildTLSConfigNilOnEmptyCA(t *testing.T) { cfg, err := BuildTLSConfig(nil, nil, nil, "redis.example.com") assert.NoError(t, err) assert.Nil(t, cfg) cfg, err = BuildTLSConfig([]byte{}, nil, nil, "redis.example.com") assert.NoError(t, err) assert.Nil(t, cfg) + + clientCert, clientKey := selfSignedClientPEM(t) + _, err = BuildTLSConfig(nil, clientCert, clientKey, "redis.example.com") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "caPEM must be supplied") + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@service/redis/tls_test.go` around lines 16 - 24, Add a regression test that verifies BuildTLSConfig returns an error (and no config) when a client cert/key pair is provided but caPEM is empty; create a new test (e.g., TestBuildTLSConfigErrorOnClientCertWithoutCA) in service/redis/tls_test.go that calls BuildTLSConfig with non-empty certPEM and keyPEM bytes and nil or empty caPEM and asserts an error is returned and cfg is nil, referencing BuildTLSConfig to validate the mTLS parameter path does not silently fall back.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@service/redis/tls.go`:
- Around line 28-31: The current BuildTLSConfig returns (nil, nil) when caPEM is
empty which allows non-empty certPEM/keyPEM to be ignored and silently disables
TLS; change the early-return logic in BuildTLSConfig so that if caPEM is empty
but either certPEM or keyPEM is non-empty you return a non-nil error (with a
clear message) instead of (nil, nil); keep the existing nil return only when all
three PEMs are empty, and surface the error via BuildTLSConfig's error return so
callers cannot silently fall back to plaintext.
In `@test/integration/redisfailover/creation_test.go`:
- Around line 249-250: The call to c.redisClient.GetSentinelMonitor can return
an error and the code then appends master to the masters slice and later
accesses masters[0] without guarding for emptiness; update the calls to capture
and handle the returned error from GetSentinelMonitor (e.g., check err != nil
and fail or retry/log appropriately) and before any access to masters[0] ensure
that len(masters) > 0 (or handle the empty case) so the test cannot panic;
reference the GetSentinelMonitor call, the local variables master/err, and the
masters slice when making these changes.
- Around line 226-227: The loop currently ignores the error returned by
c.redisClient.IsMaster; update the check in creation_test.go so you capture and
handle the error from IsMaster(ip, "6379", testPass, nil) (e.g., if err != nil {
t.Fatalf("IsMaster failed for %s: %v", ip, err) } or t.Errorf and return),
instead of discarding it, so connectivity/auth failures are surfaced and the
masters slice is not miscounted; refer to the call to IsMaster and the masters
append logic to locate and modify the code.
---
Nitpick comments:
In `@service/redis/tls_test.go`:
- Around line 16-24: Add a regression test that verifies BuildTLSConfig returns
an error (and no config) when a client cert/key pair is provided but caPEM is
empty; create a new test (e.g., TestBuildTLSConfigErrorOnClientCertWithoutCA) in
service/redis/tls_test.go that calls BuildTLSConfig with non-empty certPEM and
keyPEM bytes and nil or empty caPEM and asserts an error is returned and cfg is
nil, referencing BuildTLSConfig to validate the mTLS parameter path does not
silently fall back.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fdc29f2a-340c-4207-b555-2273a2baef3a
📒 Files selected for processing (12)
.github/workflows/ci.yaml.github/workflows/helm.ymldocker/development/Containerfilemocks/service/redis/Client.gooperator/redisfailover/service/check.gooperator/redisfailover/service/heal.gooperator/redisfailover/service/heal_test.goservice/k8s/service.goservice/redis/client.goservice/redis/tls.goservice/redis/tls_test.gotest/integration/redisfailover/creation_test.go
Address review feedback from coderabbitai on service/redis/tls.go:31: the early (nil, nil) return on len(caPEM) == 0 also accepted non-empty certPEM/keyPEM, which would silently disable TLS in a misconfigured mTLS path — exactly the kind of silent plaintext fallback we already guard against on the caller side. Fail fast in BuildTLSConfig instead. New test TestBuildTLSConfigRejectsClientMaterialWithoutCA covers both cert+key without CA and cert-only without CA. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from coderabbitai on test/integration/redisfailover/creation_test.go:227: the loop in testRedisMaster discarded the IsMaster error, so a connectivity, auth or replication-state failure on any single probe would silently leave that pod out of the master count, masking the underlying failure mode. Assert NoError and skip the pod on error so the failure surfaces in the test report instead of producing a wrong count downstream. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from coderabbitai on test/integration/redisfailover/creation_test.go:250: GetSentinelMonitor errors were silently dropped in the sentinel monitoring loop, and the test then read masters[0] unconditionally — both a transient probe failure and an unexpectedly empty sentinel list would surface as a confusing index-out-of-range panic rather than a clear assertion. Assert NoError on each probe, skip the pod when it fails, and bail out with assert.NotEmpty before indexing. Signed-off-by: Arsolitt <[email protected]>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
NOT LGTM — two functional gaps in TLS-mode operation: the in-pod self-connections (liveness probe via $(hostname), redis_exporter via 127.0.0.1, sentinel monitor target 127.0.0.1) hit hosts that aren't covered by the generated certificate's SAN/IPSAN list, and the SAN list itself hardcodes cluster.local so any cluster running a custom cluster-domain (cozystack's default is cozy.local) gets a SAN mismatch on every legitimate FQDN dial.
The API surface, validation, TLS-config builder (service/redis/tls.go), Certificate spec (correct digital signature + server auth + client auth usages), and mTLS plumbing all look solid. The findings below are about which hosts the in-cluster dials use and which DNS forms the cert SAN list actually covers.
Blockers
B1: in-pod TLS dials use names that are not in the cert SAN list
Three call sites:
- Liveness probe (
operator/redisfailover/service/generator.go:590):In a StatefulSet podredis-cli --tls ... -h $(hostname) -p $PORT ping$(hostname)returns the bare pod name (rfr-<release>-0). The SAN list provides*.rfr-<release>(and the namespaced/svc/FQDN forms of that wildcard) — DNS wildcards in X.509 must consume exactly one label, so the barerfr-<release>-0does NOT match*.rfr-<release>or any of its FQDN siblings. redis-cli ≥ 6 verifies the server cert against the host in-hby default. - Exporter (
operator/redisfailover/service/generator.go:1327-1329, asserted byTestRedisExporterUsesTLSingenerator_tls_test.go):REDIS_ADDR=rediss://127.0.0.1:6379 REDIS_EXPORTER_TLS_CA_CERT_FILE=/tls/ca.crtoliver006/redis_exporterverifies the server cert by default (noREDIS_EXPORTER_SKIP_TLS_VERIFICATIONis set). 127.0.0.1 is not in the cert'sIPAddressesfield — only DNS SANs are emitted. - Sentinel config (
redisConfigTemplate/ sentinel config in generator.go):Each Sentinel dials its local Redis at 127.0.0.1 to monitor it. Same IPSAN miss.sentinel monitor mymaster 127.0.0.1 ${PORT} 2
Impact: every TLS-enabled failover should fail at startup once probes / exporter / sentinel monitor begin verifying the cert. The PR body says "Verified end-to-end against a live Kubernetes cluster: TLS PING/PONG on the master, ..." — that doesn't align with my read of the code. Possibilities:
- The test environment used a redis-cli / redis_exporter version with default
--insecure/SKIP_TLS_VERIFICATION=true— that would mask the issue. - The test exercised only the client-facing TLS port via an external client with the proper SAN, not the in-pod probes / exporter / sentinel-monitor path.
- I'm missing a flag.
Fix (any of these is sufficient; pick the simplest):
- Add
localhosttoDNSNamesand127.0.0.1(+::1) toIPAddressesinredisCertificateDNSNames. The exporter and sentinel-monitor would then verify cleanly against 127.0.0.1, and the probe could switch to-h localhost. - Alternatively, switch the probe to a full FQDN that the existing wildcard covers (e.g.
${POD_NAME}.${SERVICE_NAME}.${NAMESPACE}.svc.${CLUSTER_DOMAIN}) via downward-API env vars, and add127.0.0.1IPSAN just for the exporter/sentinel paths.
B2: SAN list hardcodes cluster.local — breaks every non-default cluster-domain
File: operator/redisfailover/service/generator.go:167 (and the wildcard equivalents):
fmt.Sprintf("%s.%s.svc.cluster.local", svc, ns)
Kubernetes clusters routinely run with a different --cluster-domain (cozystack's default is cozy.local; many on-prem clusters use cluster.internal, custom values are common). On such clusters, the FQDN form rfr-<release>.<ns>.svc.<custom-domain> is what clients actually resolve, but the cert SAN says cluster.local — TLS verify fails.
Impact: this PR is the upstream that cozystack#2729 consumes, and cozystack's default cluster-domain is cozy.local. Any cozystack user enabling the operator-managed TLS path (spec.tls.certManager) hits a SAN mismatch for every routed FQDN. The cozystack chart works around this in cozystack#2729 by using the certificateSecret (bring-your-own-Secret) mode where the chart renders its own cert-manager Certificate with the configurable cluster-domain — but the operator-managed path stays broken for non-default clusters.
Fix: read the cluster domain from an operator flag (e.g. --cluster-domain, defaulting to cluster.local for backward compat) or from a configurable field on the TLSSettings/CertManagerSettings block, and template the SAN list accordingly. The kubelet's --cluster-domain is the canonical source; passing it through as a Helm chart value is the standard pattern (cert-manager, coredns, etc. all expose this).
Non-blocking observations
BuildTLSConfiginservice/redis/tls.gois clean: TLS 1.2 minimum, proper RootCAs, ServerName required, mismatched cert/key supply rejected, mTLS support. Good shape.validateTLScorrectly enforcescertManager XOR certificateSecretand defaultsIssuerRef.Group/Kind. Good.extraSANsIP/DNS classification vianet.ParseIPis fine; consider warning when a user supplies obviously private/dangerous values like0.0.0.0or::, but that's polish.- CRD regeneration is consistent across the two
databases.spotahome.com_redisfailovers.yamlfiles (charts/redisoperator/crds/andmanifests/kustomize/base/). No drift. tls-replication yesin the redis config means slave→master replication traverses TLS — good default, matches the README/backlog intent.- The backlog note in the PR body about flipping the flag from "enable TLS" to "expose TLS port to clients" while always running TLS for inter-component traffic is a sound direction — worth tracking separately. Doesn't gate this PR.
Address review feedback from lexfrei (Blocker B1) on operator/redisfailover/service/generator.go: 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: - 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 / sentinel-exporter dialed `127.0.0.1`, and the Sentinel monitor config statement `sentinel monitor mymaster 127.0.0.1 ...` 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: - Updated TestEnsureRedisCertificate* to assert the new default IP SANs and the localhost DNS SAN, and to guard against silent regression of the default IPAddresses slice. - Strengthened TestRedisLivenessProbeUsesTLS / TestSentinelProbeUsesTLS to assert `-h localhost` and the absence of `$(hostname)`. - Updated the plain-text probe expectations in generator_test.go so the non-TLS path also dials localhost — the SAN-mismatch concern does not apply there, but consistency between TLS and plaintext probes keeps the codepath shared. Signed-off-by: Arsolitt <[email protected]>
Address review feedback from lexfrei (Blocker B2) on operator/redisfailover/service/generator.go: the FQDN SAN entries hardcoded ".svc.cluster.local", so on any Kubernetes cluster running with a non-default --cluster-domain (cozystack defaults to "cozy.local"; on-prem clusters routinely use "cluster.internal" or a site-specific value) the cert-manager Certificate produced by the operator-managed path did not cover the FQDNs clients actually resolve, and TLS verification failed for every routed FQDN dial. The bring-your-own-Secret path was unaffected because the chart renders its own Certificate with the configurable domain, but the operator-managed path stayed broken for non-default clusters. 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: - All `NewRedisFailoverKubeClient` test callers updated to pass the default "cluster.local" — purely mechanical. - New TestEnsureRedisCertificateUsesConfiguredClusterDomain asserts that with --cluster-domain=cozy.local the FQDN SANs use ".cozy.local" and that the default ".cluster.local" form is NOT silently added in addition (which would mask SAN-mismatch bugs). Signed-off-by: Arsolitt <[email protected]>
|
Aleksei Sviridkin (@lexfrei) thanks for the detailed read. Both blockers are addressed: B1 — in-pod TLS self-dials. Commit d6ee49a reshapes the SAN helper (renamed to The end-to-end PING/PONG line in the original PR body did mask this: the test client dialled the operator-facing TLS port from outside the pod, where the headless DNS SAN applied. The in-pod probes / exporter / sentinel-monitor path was not exercised separately. B2 — hardcoded cluster domain. Commit 6e4b262 adds a new Non-blocking observations 1–6 acknowledged, no changes needed on this PR; the backlog item about flipping the flag semantics is worth tracking separately. |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
LGTM on 6e4b2623. Both blockers from my prior review are addressed in two surgical commits.
-
d6ee49a fix(tls): cover in-pod TLS self-dials in the generated cert SAN list— closes B1 (liveness probe via$(hostname), redis_exporter via127.0.0.1, sentinel monitor target127.0.0.1failing TLS hostname verification).redisCertificateDNSNamesrename →redisCertificateSANsnow returns bothdnsNamesandipAddresses;localhostjoins the DNS SAN list and127.0.0.1+::1join the IP SAN list. Pod IPs are still excluded with an explicit comment ("unstable, ServerName override handles them") — correct call. 58 new lines of test coverage ingenerator_tls_test.gofor the in-pod self-dial path. -
6e4b262 feat(tls): add --cluster-domain flag for cert SAN templating— closes B2 (hardcodedcluster.localbreaking non-default cluster-domain setups like cozystack'scozy.local). New--cluster-domainoperator flag plumbs throughCMDFlags→Config→ operator factory →generateRedisCertificate→redisCertificateSANs(rf, clusterDomain). Default iscluster.localso existing deployments that never set the flag keep working. Tests cover the parameterization (generator_tls_test.go+77 lines).
API surface, validation, BuildTLSConfig, mTLS plumbing, cert usages (digital signature + server auth + client auth + key encipherment) all carry over from prior review. The fork is ready to tag — once v1.4.0 lands, the downstream cozystack/cozystack#2729 can also move on its B1.
Adds end-to-end TLS for RedisFailover by integrating cert-manager.
When
spec.tls.enabled: true, the operator provisions a Certificate through the user-supplied Issuer/ClusterIssuer, mounts the resulting Secret into every Redis and Sentinel pod, and reconfigures the processes to terminate clients on--tls-portwith the plaintext--portdisabled. Sentinel monitoring and slave→master replication traverse the TLS listener as well, so once enabled there is no plaintext path left in the failover.Bring-your-own-Secret remains supported: omit the
certManagerblock and pointspec.tls.certificateSecret.secretNameat an externally managed Secret.Changes proposed on the PR:
spec.tls(enabled, certManager, certificateSecret, authClients, extraSANs) to the RedisFailover CRD with kubebuilder validation; regenerate deepcopy, clientset, informers, listers and CRD manifest via slok/kube-code-generator v0.9.0.--tls-port,--port 0, cert/key/CA paths andtls-replication yesfor Redis pods; same treatment for Sentinel; plumb the in-process Redis client through the TLS connection so role probes, sentinel queries and reconfig RPCs use the same trust chain; reconcile a cert-manager Certificate with per-pod and per-service SANs, plus optionalextraSANsfor external hostnames or IPs (auto-classified into DNS vs IP SANs).envarray on the Deployment template so the WatchListClient feature gate can be flipped off where required.example/redisfailover/tls.yamlcovering the typical ClusterIssuer-based topology with commented overrides forduration,renewBefore,privateKeyandextraSANs..golangci.yml(golangci-lint v2 schema) so the pipeline keeps running under Go 1.26; bump the integration-test matrix to Kubernetes 1.34/1.35/1.36 (older API servers hang the cache sync because client-go 0.36 enables WatchListClient by default and earlier servers never deliver the required BOOKMARK event); refresh checkout, setup-go, setup-helm, setup-minikube and chart-releaser-action to their current major versions.Verified end-to-end against a live Kubernetes cluster: TLS PING/PONG on the master, slave replication online over TLS, Sentinel monitoring the master over TLS, and the plaintext port refusing connections.
Backlog (out of scope for this PR)
Consider moving inter-component traffic — replication, sentinel-to-redis and operator-to-redis — to TLS unconditionally, regardless of the
tls.enabledflag, and repurposing the flag to control only whether the TLS port is exposed to client connections. The proposed default would beport 6379plaintext for clients plusport 6380TLS for replication, sentinel and the operator, with the flag flipping the client-facing port between 6379 and 6380 in a mutually-exclusive fashion.This would harden the default posture (replication traffic always encrypted, cert validity becomes a cluster-availability concern rather than a client UX concern) but would make cert-manager — or an in-operator PKI implementation — a hard dependency rather than optional. Worth a separate design discussion before tackling.
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Tests