Confluence: Freeze Page & Attachment Deletion
Freezes the two irreversible Confluence deletion tools on the agent channel: confluence delete page and confluence delete attachment.
- Direction
- ingress
- Rego package
confluence.ingress.freeze_page_deletion- App
- confluence
- Bundles
- atlassiansoc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:83c0636b7f0f1496216dd9220e12e0da1a8cd91cfb4875d981cd911d0ab97483
confluenceatlassianfreeze-destructive-opsdata-protectioningresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on the frozen delete tools, allow everything else
Package: confluence.ingress.freeze_page_deletion
What it does
Freezes the two irreversible Confluence deletion tools on the agent channel:
confluence_delete_page and confluence_delete_attachment. Any tool call
whose name ends with one of those suffixes is denied for all callers, with an
optional break-glass exemption for a placeholder confluence-admins group.
Every other Confluence tool — reads, searches, page creates/updates, comment
and label writes, attachment uploads — passes through untouched.
The check runs at ingress, before the call reaches the Confluence MCP server, so a frozen deletion never executes: the page or attachment survives an injected prompt or an erring agent. When a human genuinely needs to delete wiki content, they do it through the Confluence UI (or, for maintenance, from an account in the break-glass group).
These two tools exist only on the community sooperset/mcp-atlassian server. The official Atlassian Rovo MCP server exposes no delete tools at all (verified in the app landscape note — it cannot delete pages, attachments, issues, or comments). So on official-connector deployments this policy is a zero-cost safety net that never fires; on community deployments it is the control that actually stops destructive agent behaviour.
Compliance alignment
This policy instantiates the record-freeze family (PF-06,
freeze-destructive-ops) on Confluence's deletion surface, and supports
alignment with:
- SOX §802 / 18 U.S.C. §1519 — anti-destruction/alteration of records: the agent cannot destroy wiki pages or attachments that may be relied on as business records over the MCP path (Enforceable in the coverage matrix). Also supports §802 / Rule 2-06 retention/legal-hold on evidence paths by keeping the agent from purging preserved content.
- SOC 2 PI1.5 — integrity of stored records: denies agent-driven deletion that would compromise the completeness of stored wiki content.
- HIPAA §164.312(c) — integrity (anti-alteration) of ePHI that may live in Confluence pages/attachments; §164.530(c) — privacy safeguards, by removing an irreversible destruction path from the agent channel.
- GDPR Art. 5(1)(d) — accuracy: prevents mass agent-driven loss of records (an accuracy/availability failure) by freezing bulk deletion over MCP.
Tool name matching
The gateway prefixes tool names with the configured MCP server name (e.g.
mcp-atlassian-confluence_delete_page), and that prefix is not standardized.
The policy matches on the tool-name suffix so it stays portable across
server-name conventions, and lowercases the name first so casing never causes
a silent miss:
*confluence_delete_page*confluence_delete_attachment
These are the community sooperset/mcp-atlassian names (verified in the
landscape note). The official Rovo server has no delete tools, so there is no
official-naming variant to add. If your community deployment renames these
tools, add the new suffixes to destructive_tool_suffixes in policy.md.
The name is read from both the PARC field (input.resource.name) and the
legacy alias (input.payload.name) via object.get chains, and the two are
matched independently — a request that omits the resource block, or one
carrying a malformed (non-string) value in either field, still cannot skip the
match. Each field is coerced to a lowercased, whitespace-trimmed string (a
number, null, array, or object resolves to the empty string), so a non-string
value in one field can never suppress a genuine delete suffix in the other,
and a name padded with trailing spaces/newlines ("confluence_delete_page \n")
still matches the frozen suffix.
Argument shape
This policy makes its decision purely from the tool name and the caller's
identity — it does not read input.payload.args at all. That means a frozen
delete call is denied even if it arrives with missing, empty, or unexpected
arguments; there is no arg shape an attacker can craft to slip past it.
Identity / break-glass
An optional allow if branch exempts members of a placeholder
confluence-admins group, read from the caller's IdP-issued groups claim via
object.get(object.get(input.subject, "claims", {}), "groups", []). This lets a
designated maintenance account perform deletions through the agent during
planned cleanup without detaching the policy. The check fails closed: a caller
with no subject, no claims, no groups, or no matching group is not
exempt and the deletion is denied. The groups claim is honored only when
it is a JSON array (is_array guard): a bare string, or an object/map shape
such as {"role": "confluence-admins"}, is rejected — without that guard a
Rego some group in groups would iterate an object's values and let a
map-shaped claim satisfy the grant.
To freeze deletions for everyone (including admins), delete the break-glass
allow if branch — the default allow := false then denies all callers on the
two frozen tools.
Examples
Denied (agent tries to delete a page)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-confluence_delete_page", "type": "tool" },
"subject": { "sub": "google-apps|agent@acme.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "mcp-atlassian-confluence_delete_page",
"args": { "page_id": "123456" }
}
}
}
allow = false,
reason = "Deleting Confluence pages or attachments is frozen on the agent channel. ...".
Allowed (non-destructive Confluence write)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-confluence_update_page", "type": "tool" },
"payload": {
"name": "mcp-atlassian-confluence_update_page",
"args": { "page_id": "123456", "title": "Runbook", "body": "..." }
}
}
}
allow = true, no reason.
Allowed (break-glass admin deletes during maintenance)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "mcp-atlassian-confluence_delete_page", "type": "tool" },
"subject": { "sub": "google-apps|admin@acme.com", "claims": { "groups": ["confluence-admins"] } },
"payload": {
"name": "mcp-atlassian-confluence_delete_page",
"args": { "page_id": "123456" }
}
}
}
allow = true, no reason.
Composition
Single-purpose by design. Useful companions in the
atlassian bundle:
- A parallel Jira freeze policy for
jira_delete_issue/jira_remove_issue_link(this policy allows those through — it only fences Confluence deletion). jira/deny-write-sensitive-projects— write-side fencing for designated Jira projects.- A publication-control policy on
*confluence_create_page/*confluence_update_pageto keep drafts from publishing org-wide.
Known limitations
- Exact-suffix match only. The rule fires on names ending in
confluence_delete_page/confluence_delete_attachment. A future community tool with a different name (e.g.confluence_delete_pagesorconfluence_purge_page) would not be covered — add its suffix if your server exposes one. It does not fire on names where the delete verb is embedded mid-string (e.g.confluence_delete_page_tree). - Invisible-character padding is not normalized. Names are lowercased and whitespace-trimmed before matching, so trailing spaces/newlines cannot dodge the suffix — but a name padded with a non-whitespace invisible character (e.g. a zero-width space, U+200B) does evade the match. This is not an exploitable deletion path: MCP servers dispatch tools by exact name, so the padded name is an unknown tool and the call fails at the server rather than deleting anything. Recorded here so the residual is explicit.
- Content blanking and moves are not deletion.
confluence_update_page/confluence_update_page_sectioncan overwrite or empty a page's body, andconfluence_move_pagecan relocate a page — all pass through this policy. Those edits are versioned and recoverable from page history (unlike the frozen delete tools, which are irreversible), which is why they are out of scope; pair with a publication-control or write-fencing policy if edit-level protection is needed. - Group names are placeholders — replace
confluence-adminswith your IdP's group name at import time. The break-glass branch is only as trustworthy as thegroupsclaim your IdP issues; if callers can self-assert group membership, remap it to a claim your IdP controls, or remove the branch entirely to freeze deletions for all callers. - Break-glass requires an array-valued
groupsclaim. The exemption only honorsgroupswhen it is a JSON array (is_arrayguard). An IdP that flattens a single group into a bare string ("groups": "confluence-admins"), or emits an object/map shape, will not satisfy the break-glass branch, so that admin is denied — a fail-closed, safe-side outcome, but if your IdP emits string-valued groups, normalize the claim to an array before relying on break-glass. - Community-server-specific. These tool names exist only on sooperset/mcp-atlassian. On official Rovo deployments the policy is inert (no delete tools exist), which is intended defense-in-depth, not a gap.
- Deletion via other paths is out of reach. This only covers the MCP channel. A user deleting a page in the Confluence web UI or via the REST API is outside the gateway's scope by design.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package confluence.ingress.freeze_page_deletion
# Deny-by-default: only the explicit allow rules below permit a request. Every
# non-destructive tool is allowed; the two frozen delete tools are allowed only
# for the break-glass admin group.
default allow := false
# -----------------------------------------------------------------------------
# FROZEN TOOLS: irreversible Confluence deletion tools. These names exist only
# on the community sooperset/mcp-atlassian server; the official Rovo server has
# no delete tools, so the rule simply never fires there. Suffix matching keeps
# the policy portable across gateway server-name prefixes (e.g.
# `mcp-atlassian-confluence_delete_page`).
# -----------------------------------------------------------------------------
destructive_tool_suffixes := {
"confluence_delete_page",
"confluence_delete_attachment",
}
# -----------------------------------------------------------------------------
# BREAK-GLASS: members of this IdP group may still delete (planned maintenance).
# Placeholder — remap to your IdP's group name at import time. Delete the
# `allow if { is_destructive_tool; is_admin }` branch below to freeze deletion
# for everyone, including admins.
# -----------------------------------------------------------------------------
admin_group := "confluence-admins"
# Tool name is read via object.get chains from BOTH the PARC field
# (input.resource.name) and the legacy alias (input.payload.name), so a request
# that somehow omits the resource block still cannot skip matching (red-team
# hardening: missing resource must not fail open). name_of coerces to a
# lowercased, whitespace-trimmed string: a missing OR non-string value (number,
# null, array, object) resolves to "" rather than leaving the rule undefined —
# a non-string resource.name must never suppress a real delete suffix in
# payload.name — and trim_space stops trailing-space/newline padding
# ("confluence_delete_page \n") from dodging the suffix match (red-team fix).
name_of(key) := trim_space(lower(v)) if {
v := object.get(object.get(input, key, {}), "name", "")
is_string(v)
}
name_of(key) := "" if {
v := object.get(object.get(input, key, {}), "name", "")
not is_string(v)
}
resource_name := name_of("resource")
payload_name := name_of("payload")
# The two names are matched independently. Keeping separate branches means a
# malformed (non-string) value in one field cannot suppress a real delete
# suffix in the other.
is_destructive_tool if {
some suffix in destructive_tool_suffixes
endswith(resource_name, suffix)
}
is_destructive_tool if {
some suffix in destructive_tool_suffixes
endswith(payload_name, suffix)
}
# Groups from the caller's IdP-issued JWT. Fail closed: a missing subject,
# missing claims, missing groups, or a non-array groups value all yield "not
# admin", so the frozen deletion is denied.
is_admin if {
claims := object.get(input.subject, "claims", {})
groups := object.get(claims, "groups", [])
# Only honor an array-shaped groups claim. `some g in obj` iterates an
# object's VALUES, so without this guard an object-shaped claim such as
# {"role": "confluence-admins"} would silently satisfy the break-glass
# grant. is_array forces every non-array shape (string, object, number,
# null) to fail closed — no exemption.
is_array(groups)
some g in groups
g == admin_group
}
# Allow everything that is not a frozen deletion tool.
allow if {
not is_destructive_tool
}
# Break-glass: allow a frozen deletion for members of the admin group.
allow if {
is_destructive_tool
is_admin
}
# Deny reason for a non-admin caller hitting a frozen deletion tool.
reasons contains "Deleting Confluence pages or attachments is frozen on the agent channel. Deletions are irreversible, so a human must perform them in the Confluence UI. Contact your Confluence admins if this deletion is required." if {
is_destructive_tool
not is_admin
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa