dtwo Policy Store

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_page to 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_pages or confluence_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_section can overwrite or empty a page's body, and confluence_move_page can 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-admins with your IdP's group name at import time. The break-glass branch is only as trustworthy as the groups claim 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 groups claim. The exemption only honors groups when it is a JSON array (is_array guard). 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)