> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deletion approvals

> Deleting a guest cluster can require a second person — how a request is raised, reviewed and executed, what a token can and cannot do, and the guard that refuses a stale approval.

Deleting a guest cluster is the one operation in Celum that can require **two people**: one raises a request, another approves it. Everything else a permission grants happens the moment it is invoked.

Requests are reviewed at **/iam/approvals**, and — when Slack is configured — on an approval card with **Approve** and **Reject** buttons in a channel.

## The flow

```mermaid theme={null}
flowchart LR
    D[Delete a cluster] --> B{bypass?}
    B -->|interactive session<br/>with the grant| X[deleted immediately]
    B -->|otherwise| R[202 approval-required<br/>request created]
    R --> V[visible to the requester<br/>and to approvers]
    V --> A[Approve] --> U{cluster still<br/>the same object?}
    V --> J[Reject<br/>comment required]
    U -->|yes| C[deleted]
    U -->|no| S[stale — nothing deleted]
```

A delete that is gated returns **`202`** with `status: approval-required` and the request itself — not an error. The cluster is untouched until somebody decides.

## Who can do what

| | Who | Notes |
| - | - | - |
| **Raise** a request | anyone who may delete the cluster | the delete call itself becomes the request |
| **Approve / reject** | a holder of `cluster:ApproveDelete` on that cluster | **never the requester** — self-approval is refused with `403` |
| **Bypass entirely** | a holder of `cluster:DeleteWithoutApproval`, in an **interactive session** | the delete happens immediately, with no request |
| **See a request** | its requester, or anyone who could approve it | filtered per row, not per page |

> [!WARNING]
>
> **An API token can never bypass approval, and can never approve.** Both are refused at the door: a token's delete is always accepted as a request for a human, and a token's approve is rejected with *"Deletion requests must be reviewed from an interactive session"*.
>
> Granting `cluster:DeleteWithoutApproval` to a token therefore changes nothing. A token that needs to remove clusters unattended cannot be given that ability by permission — the review is a property of the session, not of the policy.

> [!NOTE]
>
> `cluster:ListDeletionRequests` exists and is carried by two built-in policies, but **nothing checks it**. The listing endpoint authorises **per row** — it returns the requests you raised plus the ones you may approve — so the action is there to keep policy validation from rejecting a name the platform itself hands out, not to gate anything.

## A request

| Field | Meaning |
| - | - |
| `clusterName`, `namespace`, `supervisor` | what would be deleted |
| `strategy` | how: `capi` or `legacy`. A `gitops` request queued before 1.202.0 now fails with the reason, because Celum no longer writes to the repository |
| `requesterEmail` / `requesterName` | who asked |
| `reason` | why — supplied at request time |
| `status` | where it is — see the states below |
| `requestedAt` / `expiresAt` | raised when, and **valid for 24 hours** |
| `approverEmail` / `approverName` / `decidedAt` | who decided, and when |
| `decisionComment` | **required to reject**, optional to approve |

A rejection without a comment is refused with `400`. That asymmetry is deliberate: approving says "this is what we agreed", rejecting says "not this", and only the second one needs to explain itself to the person who asked.

### Its states

| State | Meaning | Page tab |
| - | - | - |
| `pending` | waiting for an approver | Pending |
| `expired` | nobody decided within 24 hours | History |
| `cancelled` | the requester withdrew it while pending, or someone with the bypass deleted the cluster directly | History |
| `rejected` | an approver refused it, with a comment | History |
| `approved` | approved, waiting for the deletion to run | In progress |
| `executing` | the deletion is running | In progress |
| `completed` | the cluster is gone (also when it was already gone) | History |
| `stale` | the cluster is no longer the object the request named; nothing was deleted (below) | History |
| `failed` | the deletion failed three times; the last error is kept | History |

A failed attempt returns the request to `approved` and it is tried again, up to three attempts in all.

## The guard that makes an old approval safe

A request records the target cluster's **UID** when it is raised, and the UID is checked again at execution.

If they differ, the deletion is marked **`stale`** and **nothing is deleted.** That is not a corner case: a request is valid for 24 hours, and a cluster can be destroyed and recreated under the same name within that window. Name and namespace would still match, and approving would delete the *new* cluster while everyone believed they were retiring the old one.

If the cluster is gone entirely by the time approval lands, the request settles as **`completed`** — the intent was satisfied, just not by this call.

> [tab] UI
>
> **/iam/approvals** lists what you may see. A cluster's own page shows its active request, so you do not have to go looking to find out why a delete has not happened.

> [tab] API
>
> ```text theme={null}
> GET  /api/cluster-deletion-requests?status=&supervisor=&limit=
> GET  /api/supervisors/{name}/clusters/{cluster}/deletion-request?namespace=
> POST /api/supervisors/{name}/clusters/{cluster}/deletion-requests/{id}/approve
> POST /api/supervisors/{name}/clusters/{cluster}/deletion-requests/{id}/reject
> POST /api/supervisors/{name}/clusters/{cluster}/deletion-requests/{id}/cancel
> ```
>
> The request is created by the ordinary cluster delete, which answers `202` instead of performing it.

> [tab] Slack
>
> A configured Slack app posts an approval card — cluster, supervisor, requester, reason — with **Approve** / **Reject** buttons and a link back into Celum. See [Slack notifications](https://docs.celum.io/platform-health/slack-notifications.md).

> [tab] MCP
>
> **Nothing here has a tool, and that is the design.** An assistant can neither raise a bypassing delete nor decide someone else's request — the same boundary the API token rule draws.

## Permissions

| Task | Action | KRN |
| - | - | - |
| Approve or reject a request | `cluster:ApproveDelete` | `krn:celum:supervisor:<supervisor>:cluster:<cluster>` |
| Delete without raising one | `cluster:DeleteWithoutApproval` | `krn:celum:supervisor:<supervisor>:cluster:<cluster>` |

Both are cluster-scoped, so approval authority can be granted for one namespace's clusters and not another's. The managed policies that carry them are `K8sGateDeleteApprover` and `K8sGateClusterDeleteBypass` — see [Authentication & IAM](https://docs.celum.io/get-started/authentication.md).

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| A delete returns `202` instead of deleting | working as intended — it became a request. Someone else has to approve it |
| A script with `cluster:DeleteWithoutApproval` still only creates requests | it authenticates with an API token. The bypass is unavailable to tokens by design, whatever the policy says |
| Approving your own request returns `403` | the requester cannot be the approver. That is the entire point of the feature |
| A rejection returns `400` | rejections require a comment |
| An approval reports `stale` and nothing was deleted | the cluster is no longer the object the request was raised against — destroyed and recreated under the same name. Raise a fresh request if you still mean it |
| The approvals page is empty for an administrator | rows are filtered per request: you see what you raised and what you may approve. A cluster outside your grants is not listed |
| Everything about approvals returns `503` | no `DATABASE_URL`. Requests live in Celum's database |

## Related

> [card] [Authentication & IAM](https://docs.celum.io/get-started/authentication.md)
>
> The managed policies that carry approval and bypass.

> [card] [Delegate to a team](https://docs.celum.io/get-started/delegate-to-a-team.md)
>
> Handing out these grants deliberately, and what each one costs.

> [card] [Slack notifications](https://docs.celum.io/platform-health/slack-notifications.md)
>
> Approving from a channel, and how the button click gets back.

> [card] [Audit log](https://docs.celum.io/administration/audit-log.md)
>
> The record of who decided what, alongside every other mutation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.