Skip to content

Add deletion protection webhook for critical platform resources - #2402

Closed
myasnikovdaniil wants to merge 11 commits into
mainfrom
feature/deletion-protection
Closed

Add deletion protection webhook for critical platform resources#2402
myasnikovdaniil wants to merge 11 commits into
mainfrom
feature/deletion-protection

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds a validating admission webhook that prevents accidental deletion of critical cozystack platform resources. Deleting tenant-root cascades to all child tenants, deleting cozy-system kills the operator, deleting LinstorCluster takes down all storage — there was no admission-level guardrail preventing this.

Design

  • Label-based: resources with cozystack.io/deletion-protected: "true" are protected
  • objectSelector filtering: webhook only fires for labeled resources — zero overhead on everything else
  • Integrated into lineage-controller-webhook: reuses existing DaemonSet, Service, TLS certificates, RBAC
  • failurePolicy: Fail: if webhook is down, protected resources cannot be deleted (safe default)

Protected resources

Resource Kind
tenant-root Namespace + HelmRelease
cozy-system Namespace
cozystack-version ConfigMap
cozystack-packages OCIRepository
linstorcluster LinstorCluster
letsencrypt-prod, letsencrypt-stage, selfsigned-cluster-issuer ClusterIssuers
packages.cozystack.io, packagesources.cozystack.io CRDs (via Go code)

Bypass mechanism

Two deliberate steps required:

kubectl label namespace tenant-root cozystack.io/deletion-protected-
kubectl delete namespace tenant-root

Release note

Added deletion protection webhook preventing accidental deletion of critical platform resources (tenant-root, cozy-system, LinstorCluster, CRDs, ClusterIssuers, platform ConfigMap and OCIRepository). Protection is label-based and can be bypassed by removing the cozystack.io/deletion-protected label.

Test plan

  • Unit tests for webhook handler (14 tests passing)
  • go build for lineage-controller-webhook binary
  • go test ./internal/crdinstall/ passes with label injection
  • Deploy to dev cluster and verify kubectl delete ns tenant-root is denied
  • Verify bypass works: remove label, then delete succeeds
  • Verify non-protected resources are unaffected

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Deletion protection mechanism now enforces label-based safeguards on critical resources including namespaces, configurations, Helm releases, CRDs, certificate issuers, and storage clusters. Protected resources must have the deletion-protection label explicitly removed before deletion is allowed.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Label-based objectSelector (cozystack.io/deletion-protected=true) ensures
zero overhead for non-protected resources. Reuses the existing
lineage-controller-webhook Service and TLS certificates.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
… HelmRelease

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…ap and cozystack-packages OCIRepository

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…hitespace

- LinstorCluster is cluster-scoped: fix webhook rule scope to Cluster,
  remove namespace from kindToResourceArg and test
- OCIRepository webhook rule: scope "*" -> Namespaced
- Unexport DeletionProtectedLabel constant (only used within package)
- Normalize trailing whitespace in cert-manager-issuers template

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@dosubot dosubot Bot added size/L This PR changes 100-499 lines, ignoring generated files kind/feature Categorizes issue or PR as related to a new feature labels Apr 15, 2026
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

A deletion protection mechanism is implemented by adding an admission webhook handler that denies DELETE requests for resources labeled cozystack.io/deletion-protected: "true", registering it in the lineage controller, configuring it via a ValidatingWebhookConfiguration, and labeling critical resources with the protection marker.

Changes

Cohort / File(s) Summary
Webhook Handler Implementation
cmd/lineage-controller-webhook/main.go, internal/lineagecontrollerwebhook/validate_deletion.go, internal/lineagecontrollerwebhook/validate_deletion_test.go
Introduces DeletionProtectionWebhook admission handler that inspects DELETE requests and denies deletion of resources with the cozystack.io/deletion-protected label, with helper logic to format resource identifiers for error messages. Includes comprehensive unit tests covering multiple resource kinds and non-DELETE operations.
CRD Protection Labeling
internal/crdinstall/install.go
Mutates all parsed CustomResourceDefinition objects during installation to ensure the cozystack.io/deletion-protected: "true" label is set, preventing accidental CRD deletion.
Webhook Configuration
packages/system/lineage-controller-webhook/templates/validatingwebhookconfiguration.yaml
Adds ValidatingWebhookConfiguration named deletion-protection that intercepts DELETE requests for labeled resources across cluster and namespace scopes, routing to the webhook service at /validate-deletion with fail-closed policy and CA injection.
Resource Protection Labels
packages/core/installer/templates/cozystack-operator.yaml, packages/core/platform/templates/cozystack-version.yaml, packages/core/platform/templates/repository.yaml, packages/system/cert-manager-issuers/templates/cluster-issuers.yaml, packages/system/cozystack-basics/templates/tenant-root.yaml, packages/system/linstor/templates/cluster.yaml
Applies cozystack.io/deletion-protected: "true" label to critical system resources including the cozy-system Namespace, ConfigMaps, HelmReleases, ClusterIssuers, and LinstorCluster to enable webhook-based deletion protection.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A whisker-twitching webhook now guards the gates,
Deletion-protected labels seal the fates,
"Remove me first!" the resources cry with glee,
Preventing accidents, wild and free,
Safety in labels, as rabbits decree! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a deletion protection webhook for critical resources.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/deletion-protection

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 and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a crucial safety mechanism for the Cozystack platform by implementing a deletion protection webhook. This webhook prevents the accidental removal of vital system components, which could lead to significant operational disruptions. The protection is applied to resources explicitly marked with a specific label, ensuring that critical infrastructure elements are safeguarded while allowing flexibility for other resources. This enhancement significantly improves the platform's resilience against unintended destructive operations.

Highlights

  • Deletion Protection Webhook: Implemented a validating admission webhook to prevent accidental deletion of critical Cozystack platform resources.
  • Label-Based Protection: Resources are protected if they carry the cozystack.io/deletion-protected: "true" label.
  • Efficient Filtering: The webhook uses objectSelector filtering, ensuring zero overhead on non-labeled resources.
  • Integrated into Existing Infrastructure: The webhook reuses the existing lineage-controller-webhook DaemonSet, Service, TLS certificates, and RBAC.
  • Safe Default Failure Policy: Configured with failurePolicy: Fail, meaning if the webhook is down, protected resources cannot be deleted.
  • Protected Resources: Key resources like tenant-root (Namespace + HelmRelease), cozy-system (Namespace), cozystack-version (ConfigMap), cozystack-packages (OCIRepository), LinstorCluster, specific ClusterIssuers, and CRDs are now protected.
  • Bypass Mechanism: Deletion protection can be bypassed by explicitly removing the cozystack.io/deletion-protected label from a resource.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/lineagecontrollerwebhook/validate_deletion_test.go`:
- Around line 132-134: The test currently lets denied cases pass when
resp.Result is nil; change the assertion in the test handling of !tt.wantAllowed
so it fails if resp.Result is nil and otherwise verifies resp.Result.Code equals
tt.wantCode. Concretely, in the block referencing resp.Result, tt.wantAllowed
and tt.wantCode (around the existing if !tt.wantAllowed && resp.Result != nil &&
resp.Result.Code != tt.wantCode), replace that compound condition with a
two-step check: if !tt.wantAllowed then first assert resp.Result != nil (error
if nil, mentioning tt.wantCode), and only then assert resp.Result.Code ==
tt.wantCode.

In `@packages/core/platform/templates/cozystack-version.yaml`:
- Around line 9-10: Existing installations won't get the deletion-protection
label because the template only renders when the ConfigMap is missing; update
the cozystack-version ConfigMap template to always render metadata.labels
(including cozystack.io/deletion-protected: "true") while preserving any
existing data/version value on the ConfigMap named "cozystack-version" so
upgrades backfill the label without clobbering the current version; specifically
change the template rendering logic that writes metadata (cozystack-version
ConfigMap) to merge or patch labels rather than only creating the object when
absent and ensure the label key cozystack.io/deletion-protected is always
present.

In
`@packages/system/lineage-controller-webhook/templates/validatingwebhookconfiguration.yaml`:
- Line 6: The YAML annotation cert-manager.io/inject-ca-from currently emits an
unquoted templated value causing lint/parse failures; modify the
ValidatingWebhookConfiguration template to wrap the entire annotation value in
double quotes so the rendered string "{{ .Release.Namespace
}}/lineage-controller-webhook" is quoted and valid YAML (update the
cert-manager.io/inject-ca-from entry in the template to use quoted templating).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c35978e4-9df9-410e-aa5d-1cd47eb9ddd3

📥 Commits

Reviewing files that changed from the base of the PR and between ad52992 and 2157452.

📒 Files selected for processing (11)
  • cmd/lineage-controller-webhook/main.go
  • internal/crdinstall/install.go
  • internal/lineagecontrollerwebhook/validate_deletion.go
  • internal/lineagecontrollerwebhook/validate_deletion_test.go
  • packages/core/installer/templates/cozystack-operator.yaml
  • packages/core/platform/templates/cozystack-version.yaml
  • packages/core/platform/templates/repository.yaml
  • packages/system/cert-manager-issuers/templates/cluster-issuers.yaml
  • packages/system/cozystack-basics/templates/tenant-root.yaml
  • packages/system/lineage-controller-webhook/templates/validatingwebhookconfiguration.yaml
  • packages/system/linstor/templates/cluster.yaml

Comment on lines +132 to +134
if !tt.wantAllowed && resp.Result != nil && resp.Result.Code != tt.wantCode {
t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make forbidden-response assertions fail when Result is missing.

At Line 132, denied responses with resp.Result == nil currently pass. That can hide regressions in webhook error construction.

Proposed fix
-			if !tt.wantAllowed && resp.Result != nil && resp.Result.Code != tt.wantCode {
-				t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
-			}
+			if !tt.wantAllowed {
+				if resp.Result == nil {
+					t.Fatalf("Result is nil for denied response")
+				}
+				if resp.Result.Code != tt.wantCode {
+					t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
+				}
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !tt.wantAllowed && resp.Result != nil && resp.Result.Code != tt.wantCode {
t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
}
if !tt.wantAllowed {
if resp.Result == nil {
t.Fatalf("Result is nil for denied response")
}
if resp.Result.Code != tt.wantCode {
t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/lineagecontrollerwebhook/validate_deletion_test.go` around lines 132
- 134, The test currently lets denied cases pass when resp.Result is nil; change
the assertion in the test handling of !tt.wantAllowed so it fails if resp.Result
is nil and otherwise verifies resp.Result.Code equals tt.wantCode. Concretely,
in the block referencing resp.Result, tt.wantAllowed and tt.wantCode (around the
existing if !tt.wantAllowed && resp.Result != nil && resp.Result.Code !=
tt.wantCode), replace that compound condition with a two-step check: if
!tt.wantAllowed then first assert resp.Result != nil (error if nil, mentioning
tt.wantCode), and only then assert resp.Result.Code == tt.wantCode.

Comment on lines +9 to +10
labels:
cozystack.io/deletion-protected: "true"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Existing installations may never receive deletion protection for this ConfigMap

Because this manifest is rendered only when the ConfigMap does not already exist, upgrades won’t backfill the new label on existing cozystack-version objects. Those existing objects stay deletable because the webhook uses label-based selection.

Suggested fix (preserve current version while always rendering metadata)
 {{- $configMap := lookup "v1" "ConfigMap" .Release.Namespace "cozystack-version" }}
-{{- if not $configMap }}
+{{- $version := .Values.migrations.targetVersion | toString }}
+{{- if $configMap }}
+{{- $version = dig "data" "version" $version $configMap | toString }}
+{{- end }}
 ---
 apiVersion: v1
 kind: ConfigMap
 metadata:
   name: cozystack-version
   namespace: {{ .Release.Namespace }}
   labels:
     cozystack.io/deletion-protected: "true"
   annotations:
     helm.sh/resource-policy: keep
 data:
-  version: {{ .Values.migrations.targetVersion | quote }}
-{{- end }}
+  version: {{ $version | quote }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/platform/templates/cozystack-version.yaml` around lines 9 - 10,
Existing installations won't get the deletion-protection label because the
template only renders when the ConfigMap is missing; update the
cozystack-version ConfigMap template to always render metadata.labels (including
cozystack.io/deletion-protected: "true") while preserving any existing
data/version value on the ConfigMap named "cozystack-version" so upgrades
backfill the label without clobbering the current version; specifically change
the template rendering logic that writes metadata (cozystack-version ConfigMap)
to merge or patch labels rather than only creating the object when absent and
ensure the label key cozystack.io/deletion-protected is always present.

metadata:
name: deletion-protection
annotations:
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/lineage-controller-webhook

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote the templated annotation value to avoid YAML parse failures.

Line 6 is currently parsed as invalid YAML by linting. Wrap the full value in quotes so template rendering and YAML parsing both succeed.

Proposed fix
-    cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/lineage-controller-webhook
+    cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/lineage-controller-webhook"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/lineage-controller-webhook
cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/lineage-controller-webhook"
🧰 Tools
🪛 YAMLlint (1.38.0)

[error] 6-6: syntax error: expected , but found ''

(syntax)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/lineage-controller-webhook/templates/validatingwebhookconfiguration.yaml`
at line 6, The YAML annotation cert-manager.io/inject-ca-from currently emits an
unquoted templated value causing lint/parse failures; modify the
ValidatingWebhookConfiguration template to wrap the entire annotation value in
double quotes so the rendered string "{{ .Release.Namespace
}}/lineage-controller-webhook" is quoted and valid YAML (update the
cert-manager.io/inject-ca-from entry in the template to use quoted templating).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a deletion protection mechanism for critical Cozystack resources by implementing a validating webhook that denies DELETE operations on resources labeled with 'cozystack.io/deletion-protected'. The changes include the webhook implementation, registration in the controller, and updates to various templates to apply the protection label to relevant resources. I have reviewed the implementation and suggest addressing the potential bypass via DeleteCollection and improving the resource identification logic in the error message.

Comment on lines +48 to +50
if req.Operation != admissionv1.Delete {
return admission.Allowed("not a DELETE operation")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The webhook currently only handles the DELETE operation. While this covers most accidental deletions, DeleteCollection (e.g., kubectl delete configmaps -l cozystack.io/deletion-protected=true) will bypass this check because it returns admission.Allowed. Consider adding support for admissionv1.DeleteCollection or explicitly documenting this limitation.

Comment on lines +84 to +106
func kindToResourceArg(kind, namespace string) string {
switch kind {
case "Namespace":
return "namespace"
case "ConfigMap":
return "configmap -n " + namespace
case "HelmRelease":
return "helmrelease.helm.toolkit.fluxcd.io -n " + namespace
case "CustomResourceDefinition":
return "crd"
case "LinstorCluster":
return "linstorcluster.piraeus.io"
case "ClusterIssuer":
return "clusterissuer.cert-manager.io"
case "OCIRepository":
return "ocirepository.source.toolkit.fluxcd.io -n " + namespace
default:
if namespace != "" {
return kind + " -n " + namespace
}
return kind
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The kindToResourceArg function uses the Kind string directly from the request. For some resources, kubectl might require the fully qualified name or a specific alias to correctly identify the resource type when applying labels. While the current mapping covers common cases, the default case (line 102) might produce suboptimal hints for less common types. Consider using lowercase for the resource type in the default case to better match standard kubectl usage.

func kindToResourceArg(kind, namespace string) string {
	switch kind {
	case "Namespace":
		return "namespace"
	case "ConfigMap":
		return "configmap -n " + namespace
	case "HelmRelease":
		return "helmrelease.helm.toolkit.fluxcd.io -n " + namespace
	case "CustomResourceDefinition":
		return "crd"
	case "LinstorCluster":
		return "linstorcluster.piraeus.io"
	case "ClusterIssuer":
		return "clusterissuer.cert-manager.io"
	case "OCIRepository":
		return "ocirepository.source.toolkit.fluxcd.io -n " + namespace
	default:
		resource := strings.ToLower(kind)
		if namespace != "" {
			return resource + " -n " + namespace
		}
		return resource
	}
}

lexfrei

This comment was marked as duplicate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the work on deletion protection — the design is solid (label-based + objectSelector filtering + fail-closed). A few things need to be addressed before this can merge.

Must fix

1. cozystack-version ConfigMap label won't be applied on upgrades

packages/core/platform/templates/cozystack-version.yaml renders only when the ConfigMap doesn't exist ({{- if not $configMap }}). On existing installations the ConfigMap already exists, so the template is skipped and the cozystack.io/deletion-protected label is never added. The resource stays unprotected.

CodeRabbit flagged this too — see its inline comment with a suggested fix.

2. Unquoted YAML annotation in webhook configuration

packages/system/lineage-controller-webhook/templates/validatingwebhookconfiguration.yaml:6:

cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/lineage-controller-webhook

The templated value must be quoted to produce valid YAML:

cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/lineage-controller-webhook"

3. DeleteCollection bypasses protection

The webhook only intercepts DELETE operations. A kubectl delete configmaps --all -n cozy-system (which issues a DeleteCollection request) will bypass the webhook entirely. Either add "DELETECOLLECTION" to the operations list in the webhook rules, or explicitly document this as a known limitation in the PR description.

Should fix

4. Test silently passes when Result is nil

internal/lineagecontrollerwebhook/validate_deletion_test.go:132:

if !tt.wantAllowed && resp.Result != nil && resp.Result.Code != tt.wantCode {

If the webhook returns a denied response with resp.Result == nil, this assertion passes silently. Split into two checks: first assert resp.Result != nil, then check the code.

5. kindToResourceArg default case doesn't lowercase

For unknown resource kinds, the hint message will contain the Kind as-is (e.g., kubectl label SomeKind ...). kubectl expects lowercase resource types. The default branch should use strings.ToLower(kind).

General note

Please don't leave bot review comments (CodeRabbit, Gemini) unaddressed — either fix them or reply explaining why they don't apply. Unanswered bot comments make it hard to tell what's been considered and what's been missed.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Another approach planned

myasnikovdaniil added a commit that referenced this pull request May 27, 2026
…sionPolicy (#2650)

## What this PR does

Adds a `ValidatingAdmissionPolicy` that blocks `DELETE` on critical
platform
objects labeled `platform.cozystack.io/no-delete=true`. Native,
in-process CEL
evaluation — no webhook DaemonSet, no Service, no TLS, no image to
maintain.
Requires Kubernetes 1.30+.

This is an alternative to the closed #2402 (admission webhook approach).

### Protected objects (labeled in this PR)

| Resource | Kind | Source |
|---|---|---|
| `cozy-system` | Namespace |
`packages/core/installer/templates/cozy-system-{namespace,labels}.yaml`
|
| `tenant-root` | Namespace |
`packages/system/cozystack-basics/templates/tenant-root.yaml` |
| `tenant-root` | HelmRelease (ns: tenant-root) | same |
| `cozystack-version` | ConfigMap (ns: cozy-system) |
`packages/core/platform/templates/cozystack-version.yaml` |
| `cozystack-packages` | OCIRepository (ns: cozy-system) |
`packages/core/platform/templates/repository.yaml` |
| `letsencrypt-prod`, `letsencrypt-stage`, `selfsigned-cluster-issuer` |
ClusterIssuer |
`packages/system/cert-manager-issuers/templates/cluster-issuers.yaml` |
| `linstorcluster` | LinstorCluster |
`packages/system/linstor/templates/cluster.yaml` |
| `packages.cozystack.io`, `packagesources.cozystack.io` | CRD | label
stamped in `internal/crdinstall/install.go` |

### Bypass

```bash
kubectl label <kind> <name> platform.cozystack.io/no-delete-
kubectl delete <kind> <name>
```

### Why VAP instead of a webhook

Same outcome as #2402 but with zero runtime infrastructure: no
DaemonSet, Service,
TLS certificate, or image. The kube-apiserver evaluates the CEL
`expression: "false"` in-process and denies because `validationActions:
[Deny]`.
`objectSelector` on the binding scopes evaluation to labeled objects
only — every
other DELETE in the cluster is unaffected.

### Release note

```release-note
feat(platform): add deletion-protection guardrail (ValidatingAdmissionPolicy)
blocking DELETE on cozystack platform objects labeled
`platform.cozystack.io/no-delete=true`. Bypass by removing the label first.
Requires Kubernetes 1.30+.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Deletion-protection guardrail: objects labeled
platform.cozystack.io/no-delete=true are denied DELETE requests; removal
of the label allows deletion.

* **Chores**
* Platform migration to v43 backfills the no-delete label onto system
resources and namespaces.

* **Tools**
* Added an "unprotect" teardown utility to remove the no-delete label
for uninstall/disaster recovery.

* **Documentation**
* Helm NOTES now warns when the guardrail is inactive due to missing
cluster API.

* **Tests**
  * New tests and e2e checks validate deletion-protection behavior.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2650?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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