Skip to content

feat(redis)!: refuse external access without TLS at admission - #4639

Merged
IvanHunters merged 1 commit into
mainfrom
feat/db-external-tls-policy
Oct 6, 2026
Merged

IvanHunters merged 1 commit into
mainfrom
feat/db-external-tls-policy

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

An admission policy now refuses a Redis release that would expose a plaintext endpoint outside the cluster. Redis never turns TLS on by itself, so external: true without tls.enabled: true is exactly that case. The error names the release and a remedy that works.

The rule has three parts, because TLS is fixed when a Redis instance is created: the RedisFailover schema refuses a change, while the Redis API accepts one and the HelmRelease then fails. A spec's tls.enabled is therefore a request, not proof of what the instance runs.

  • On create, external: true needs tls.enabled: true.
  • On update, turning external on is refused whatever tls.enabled says.
  • While external stays on, tls.enabled cannot change. Before the first reconcile nothing else stops TLS being dropped from a release admitted as external with TLS. Withdrawing external in the same edit lifts the freeze, and the callers trusted by cozystack-tenant-host-policy are exempt, so an instance whose TLS was flipped earlier can still be unblocked.

Edits to a release that was already external pass, so releases exposed in plaintext before this policy keep working. The guarantee covers the Redis app. A generic publisher such as tcp-balancer can still forward to a plaintext Redis, by design.

This diverges from the Security section of the external-database-exposure proposal, which refuses only an explicit tls.enabled: false and covers every engine. That rule was written before redis had TLS and matches only postgres. Postgres is left out here on purpose: CloudNativePG keeps ssl=on and its default pg_hba accepts plaintext from any client that asks, whatever the chart's tls.enabled says, so an admission rule on that field closes nothing. That gap is #4619. The proposal update and a decision record are cozystack/community#91.

The policy follows cozystack-tenant-host-policy: a ValidatingAdmissionPolicy in cozystack-basics, enforced inside cozystack-api for the aggregated kind. Its decisions are tested in Go by compiling the shipped template with the apiserver's own CEL compiler and running Redis objects through it, including the bypass sequences found in review. A structural chart test can't show what the policy admits. The redis README, the external field description and the threat model describe the new rule.

Related: #4622 (valkey external is always plaintext), #4624 (the redis tls.enabled description implies a live switch).

Screenshots

Downstream repositories

I walked the trigger map against the diff and nothing in it fires, but I left the boxes empty because cozystack/community is reached anyway: the external-database-exposure proposal still states the old admission rule. Its update and a decision record are cozystack/community#91.

Release note

feat(redis)!: a Redis release can be exposed outside the cluster only when it is created with `tls.enabled: true`. Creating one with `external: true` and no TLS, turning `external` on for an existing instance, or changing `tls.enabled` while `external` is on is refused at admission. Instances already external without TLS keep working and stay editable.

Summary by CodeRabbit

  • New Features
    • Redis admission checks now require TLS when external access is enabled at instance creation. They block enabling external access on an existing instance and changing TLS while external access remains enabled. Disabling external access in the same update allows TLS changes.
    • Existing instances that already had external access without TLS before the policy was introduced remain editable.
  • Documentation
    • Updated Redis configuration and security guidance to explain these restrictions and TLS migration behavior.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/breaking-change Indicates the change introduces a breaking API or behaviour change kind/feature Categorizes issue or PR as related to a new feature labels Oct 1, 2026
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository: cozystack/cozystack/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 411199a9-4739-4334-8a21-5b2c320fa34d
📥 Commits

Reviewing files that changed from the base of the PR and between 868df53 and 800bb03.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: cozystack/cozystack/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 3c4d1309-3f89-4f54-ae50-b7b7226fb494

📥 Commits

Reviewing files that changed from the base of the PR and between 16a502a and 868df53.

📒 Files selected for processing (4)
  • docs/security/threat-model.md
  • packages/apps/redis/README.md
  • packages/system/cozystack-basics/templates/database-external-tls-policy.yaml
  • pkg/registry/apps/application/external_tls_policy_test.go

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

Adds a capability-gated Redis admission policy for external access and TLS changes. The policy is tested, and Redis parameter descriptions and security documentation now describe its rules.

Changes

Redis external TLS policy

Layer / File(s) Summary
Admission policy and validation
packages/system/cozystack-basics/templates/database-external-tls-policy.yaml, pkg/registry/apps/application/external_tls_policy_test.go, packages/system/cozystack-basics/tests/*
Adds Redis create and update validation, a Deny binding, CEL decision tests, Helm rendering checks, and a capability-gate assertion.
Redis contract and policy documentation
api/apps/v1alpha1/redis/types.go, packages/apps/redis/README.md, packages/apps/redis/values.schema.json, packages/apps/redis/values.yaml, packages/system/redis-rd/cozyrds/redis.yaml, docs/security/threat-model.md
Documents the creation-time TLS requirement, update restrictions, trusted caller exception, and behavior for existing external plaintext releases.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AdmissionAPI
  participant RedisPolicy
  Caller->>AdmissionAPI: Submit Redis create or update
  AdmissionAPI->>RedisPolicy: Evaluate external access and TLS rules
  RedisPolicy-->>AdmissionAPI: Allow or deny request
  AdmissionAPI-->>Caller: Return admission result
Loading

Merge Risk: ⚪ Minimal · up to 868df

The Redis admission rules appear ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 868df

The change restricts plaintext exposure without expanding tenant permissions. Its protection is conditional: legacy endpoints remain editable, privileged repair paths retain exceptions, and policy availability and concurrent updates limit the guarantee.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A tenant writer can request configuration changes for Redis resources within its delegated namespace scope. The relevant network outcome is exposure of that instance to clients able to reach its LoadBalancer. The policy grants no new cross-tenant or platform permission; privileged platform identities retain broader pre-existing authority.

Security Findings and Attack Paths

  • inferred — A stale, otherwise permitted update of an external release can race with withdrawal of external access. The existing conflict retry can restore its previously validated external=true values after external=false commits, without validating against the refreshed state. This limits the new transition guarantee, but the retry and unrestricted re-exposure predate this change; increased plaintext reachability was not established.

Trust Boundaries and Controls

  • observed — The tenant-facing control evaluates caller group identity and requested Redis values before privileged downstream writes. It deliberately does not attest deployed listener state, cover direct HelmRelease writes, or constrain all privileged repairs. Whether tenants can obtain trusted group claims or downstream write authority through other grants remains outside verified coverage.

Resilience and Maintainability Implications

  • observed — The documented recovery contract distinguishes restoring a rejected desired TLS value from migrating an actual plaintext instance. Tenant repair may require withdrawing external access; adding TLS requires a new instance and client/data migration. Existing API updates preserve live deletion metadata, controller finalizers, and suspension, but complete cleanup and reconciliation ordering were not established.

Hardening Proposals

  • proposed — Bind transition validation to the state actually committed: revalidate against a freshly reconstructed old application on conflict, or reject conflicting specification changes rather than refreshing resourceVersion unconditionally.
  • proposed — Where external-TLS enforcement is promised, check policy capability and active installation during deployment, and provide an explicit migration plan for grandfathered plaintext endpoints.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: admission now refuses external Redis access without TLS. The breaking-change marker is appropriate.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (3 skipped: 3 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A Redis release with external: true and TLS off publishes a plaintext
endpoint outside the cluster, and nothing stopped a tenant from asking
for one. The chart never infers TLS from external, because enabling it
moves Redis to a TLS-only listener, so external: true without an
explicit tls.enabled: true is exactly that case. A
ValidatingAdmissionPolicy on the Redis kind now refuses it with a
message naming the release and a remedy that works.

TLS is fixed when a Redis instance is created: the operator's
RedisFailover schema refuses a change, but the Redis API accepts it.
A spec's tls.enabled is therefore a request, not the instance's state,
and cannot prove on UPDATE that an instance has TLS; trusting it would
let a tenant flip it, expose the instance, and remove it again. So
external access can be enabled only at creation, where tls.enabled
does decide the instance, and any UPDATE turning it on is refused.

The RedisFailover schema does not exist before the first reconcile, so
a release admitted as external with TLS could have TLS dropped before
the instance is created. tls.enabled is therefore frozen while
external access stays on. Withdrawing external access in the same edit
lifts the freeze, which leaves a tenant a way to unblock an instance
whose TLS was flipped earlier, and the callers trusted by the tenant
host policy are exempt so an administrator can repair one in place.
Other edits to an instance that is already external pass, so releases
exposed in plaintext before the policy keep working.

Postgres is left out on purpose. CNPG pins ssl=on, and its default
pg_hba accepts plaintext from any client that asks, whatever the
chart's tls.enabled says; that field only decides whether the external
hostname goes into the certificate. A policy keyed on it would close
nothing.

The decisions are tested by compiling the shipped template with the
apiserver's own CEL compiler, since a structural chart test cannot
show what the policy admits.

BREAKING CHANGE: creating a Redis with external: true and without
tls.enabled: true, which applied before, is now refused, including a
GitOps re-apply of such a manifest into a fresh cluster. Turning
external on for an existing Redis is refused as well, so exposing an
instance means creating a new one with TLS, and tls.enabled can no
longer be changed on an instance with external access.

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

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict

LGTM with non-blocking notes

The guard sits at the point where redis exposure is actually decided. The external LoadBalancer in packages/apps/redis/templates/service.yaml renders only from .Values.external, and that value reaches the chart solely through the redises CR the policy gates. The policy runs inside cozystack-api the same way the already-shipped cozystack-tenant-host-policy does. One MINOR comment-bloat note below.

Findings

  • [MINOR] packages/system/cozystack-basics/templates/database-external-tls-policy.yaml:4, 25-line # prose essay ships verbatim into the applied VAP manifest

Caveats

  • Live end-to-end enforcement (a real cozystack-api denying a tenant apply) is out of scope for this hermetic review; it rests on three static signals, not an assumption: pkg/cmd/server/start.go:103-120 disables only MutatingAdmissionPolicy and keeps ValidatingAdmissionPolicy; packages/system/cozystack-api/templates/rbac.yaml:21-23 grants list/watch on validatingadmissionpolicies/validatingadmissionpolicybindings (not the mutating ones) so the plugin sources cluster policies; and cozystack-tenant-host-policy is an identical shipped VAP on the same aggregated group. A cozystack-pr-test run can confirm it live.

Recommended follow-ups (non-blocking)

  • The policy, its file, and its inline comment are named for database, but the binding matches only redises. The same plaintext endpoint outside the cluster is reachable by other first-party kinds a tenant can create (clusterroles.yaml:260-273): valkey renders the same redis-operator LoadBalancer on 6379 and has no TLS option anywhere in the chart (#4622), postgres accepts plaintext sessions regardless of the field (#4619), and a tcp-balancer can forward to a redis Service (called out as by-design in the PR body). The inline comment states the inclusion criterion as "engines whose tls.enabled decides whether the server takes plaintext"; kafka, qdrant and nats also fit that wording (an explicit tls.enabled: false keeps a plaintext listener up while external is on) yet are out of scope. Scoping to redis is deliberate (feat(redis)!, decision record in community#91), so this is a wording note rather than a defect: when the 25-line comment moves to docs (above), narrow its criterion to what the policy actually binds, otherwise the generic name reads as a platform-wide guarantee the policy does not make.
  • A non-boolean external ("true", 1) is admitted by the CEL ("true" == true evaluates to false, so newExternal is false) and the aggregated apiserver does not type-check the spec against the kind's openAPISchema (it is used only for defaulting, rest.go:112-137), so the value is refused one layer down by the redis chart's own values schema rather than at admission. Fail-closed, but the tenant sees a failed HelmRelease instead of a clear denial. Optional hardening if you want the admission layer to own that message.

{{- /* Render only where the ValidatingAdmissionPolicy API is served. Same guard as gateway-hostname-policy.yaml. */}}
{{- if .Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy" }}
---
# Refuses a database exposed outside the cluster with TLS off. The kinds are

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[MINOR] 25-line # prose essay ships verbatim into the applied VAP manifest

The block on lines 4-28 (# Refuses a database exposed...) is a # YAML comment, not a Go-template comment, so render does not strip it: helm template --show-only templates/database-external-tls-policy.yaml --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy | grep -c '^#' prints 27, so the essay lands in the object applied to every cluster and drifts stale once the CEL changes. Convert it to a {{- /* */}} Go-template comment (stripped at render, the accepted fix here) or move it to the docs site with a one-line pointer. The capability-guard comment on line 1 is already the correct {{- /* */}} form.

@IvanHunters
IvanHunters merged commit f05718b into main Oct 6, 2026
57 checks passed
@IvanHunters
IvanHunters deleted the feat/db-external-tls-policy branch October 6, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/breaking-change Indicates the change introduces a breaking API or behaviour change kind/feature Categorizes issue or PR as related to a new feature size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants