Org Skills

aem-security

Designs and hardens security for AEM as a Cloud Service across the whole stack — author access through Adobe IMS and Admin Console product profiles, principal-based ACLs and service users in repoinit, closed user groups and permission-sensitive caching, CDN traffic filter and WAF rules, purge keys, secrets and OSGi configuration values, CSRF, and security headers including CSP for client libraries.

Download .zip Raw Source
When agents use itUse this whenever the user grants or restricts access to content, writes a repoinit service user or ACL, protects a section of a site behind login, configures cdn.yaml traffic filter or WAF rules, handles an API key or credential in an OSGi config, sets CSP, HSTS or framing headers, or asks how a page stays cacheable while being permission-checked. For dispatcher filter and cache rule syntax use `aem-dispatcher`.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill aem-security -a github-copilot
# or with the org installer (adds .github/skills/aem-security):
npx -y github:AGCO-Global/org-skills add skill aem-security

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill aem-security
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill aem-security -g

Installs the aem-architecture-skills plugin, which bundles all AEM / Architecture skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-architecture-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-security -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill aem-security -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-security -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-security -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Try it — example prompts

Prompts this skill is tested against, and what a good answer includes.

  • Write the repoinit for a service user that reads product data under /content/acme and writes import results to /var/acme/import.

    create service user … with forced path system/cq:services/acme set principal ACL for … end allow jcr:read on /content/acme write privilege only on /var/acme/import bind via ServiceUserMapped and close the resolver

  • We need a members-only section on our publish site. How do we protect it and keep it fast?

    CUG on the subtree root with the allowed group and a login page protected pages are uncacheable by default permission-sensitive caching with an auth checker as the caching option alternative: public cacheable page plus client-side authenticated fragment test anonymously in a clean browser profile

  • Add a rate limit at the CDN — we're getting scraped.

    traffic filter rules in cdn.yaml through the config pipeline start in log-only mode and review what would have been blocked check your own integrations and monitoring before enforcing then switch to block and keep watching

  • Our custom form gets a 403 from AEM. Someone suggested turning off the CSRF filter.

    do not disable the filter — it removes protection site-wide fetch a token from /libs/granite/csrf/token.json and send it with the request confirm the dispatcher permits the token endpoint

  • Where do we put the API key for the pricing service our OSGi component calls?

    $[secret:…] in the OSGi configuration value set as a Cloud Manager secret environment variable never a literal in .cfg.json or in Git a key already committed must be rotated, not just deleted

  • We want to add a Content-Security-Policy but our clientlibs use inline scripts.

    start in report-only mode with a reporting endpoint move inline script and style into clientlib files nonce for what must remain inline, not unsafe-inline a looser policy for author, which needs framing and inline execution enforce on publish only after reports are clean

Skill contents

AEM Security

Targets: AEM as a Cloud Service 2026.x (IMS, principal-based ACLs, CDN traffic filters) · Verified: 2026-09 against https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/home

AEM security is layered, and each layer fails differently: the CDN decides who reaches you at all, the dispatcher decides what is even a valid URL, the repository decides who may read a node, and the code decides what it does with the session it was given. Most real incidents are not clever exploits — they are a permissive dispatcher filter, a service user with more rights than it needs, or a page that was cached without noticing it was personalised. Design every layer explicitly and assume the ones in front of it will one day be bypassed.

1. Decide first

Question Default Change when
Author access Adobe IMS with Admin Console product profiles mapped to AEM groups Never local AEM users on Cloud Service
Permissions model Group-based, granted on as few nodes as possible Per-user ACLs are unmaintainable — always a group
Service user rights set principal ACL, read-only, scoped to one path Write access only where the job genuinely writes, on that subtree only
Protecting content on publish Closed User Group (CUG) with permission-sensitive caching Public-plus-personalised fragments: cache the page, fetch the fragment client-side
Blocking abuse CDN traffic filter rules (rate, geo, IP), WAF rules where licensed Dispatcher rules are the last resort, not the first
Secrets $[secret:…] in OSGi config, set as a Cloud Manager environment variable Never a literal in .cfg.json, never in Git
Purge authorisation Purge key kept as a secret, rotated A shared, never-rotated key is a standing outage risk
Security headers Set at the CDN/dispatcher for every response Per-page headers only for genuine exceptions

2. Identity and author access

  • Authors and developers authenticate through Adobe IMS. Product profiles in the Admin Console map to AEM groups; membership is managed there, not in AEM.
  • Map profiles to a small number of AEM groups with real names (content-authors-emea, dam-editors), and grant permissions to those groups only.
  • Review the mapping when people change teams — leavers are handled by the Admin Console, but a moved author keeps whatever their old group had.

3. Repository permissions and service users

Service users are declared in repoinit. On Cloud Service the correct form places them under system/cq:services and grants principal-based ACLs:

create service user acme-price-sync with forced path system/cq:services/acme

set principal ACL for acme-price-sync
    allow jcr:read on /content/acme
end
  • with forced path system/cq:services/<project> is the Cloud Service form; set principal ACL is principal-based authorization, the default under that path.
  • One service user per job, named for the job, with the narrowest path and the fewest privileges. A service user with jcr:all on / is the AEM equivalent of running as root.
  • Grant read; grant write only on the specific subtree the job writes to.
  • loginAdministrative does not exist on Cloud Service. Code that wants it is code that has not decided what it actually needs.

Closed User Groups, permission-sensitive caching and the full repoinit patterns: references/permissions-and-service-users.md.

4. Protecting content on publish

  • A CUG restricts a content subtree to a group and redirects everyone else to a login page.
  • Protected pages are, by default, uncacheable — which is why permission-sensitive caching exists: the dispatcher caches the page and asks AEM to authorise each request through an auth checker before serving it. It trades a small request to AEM for a cache hit on the body.
  • The safer pattern for pages that are mostly public: cache the page for everyone and load the personalised fragment client-side, authenticated. A page that varies per user and is cached without a permission check is the classic data-leak incident.

5. The edge: traffic filters, WAF and purge

  • Traffic filter rules in cdn.yaml handle rate limiting, geography and IP allow/deny lists at the CDN, before the request costs you anything. WAF rules (separately licensed) add signature-based protection.
  • Start in log-only mode, read what would have been blocked, then enforce. A rate limit tuned by guesswork blocks your own integrations first.
  • Purge keys authorise cache invalidation. Treat them as secrets, rotate them, and never embed one in a client-side script.
  • Set security headers once at the edge: HSTS, X-Content-Type-Options, a framing policy, a referrer policy, and a CSP. Client libraries make CSP non-trivial — inline scripts and styles need either a nonce or extraction. Header details and a CSP rollout path: references/edge-and-headers.md.

6. Secrets and configuration

  • Secrets reach OSGi configuration as $[secret:MY_API_KEY], with the value set as a Cloud Manager secret environment variable. Non-secret per-environment values use $[env:MY_VALUE].
  • Nothing secret belongs in Git, in a content package, in a clientlib or in a log line. Scan for this in review — a key committed once must be rotated, not deleted.
  • CSRF protection is on by default for write requests; a custom form posting to AEM needs a token from /libs/granite/csrf/token.json. Disabling the filter to make a form work is a vulnerability, not a fix.

Deliverable format

## Threat summary — what is being protected, from whom, and what the exposure is
## Identity — IMS product profiles → AEM groups → permissions
## Repository — repoinit for service users and ACLs, with the path and privilege per entry
## Content protection — CUGs, caching strategy per protected area
## Edge — traffic filter and WAF rules, rate limits, purge key handling
## Headers — the header set, and the CSP with its rollout plan
## Secrets — each secret, where it is set, and its rotation owner
## Verification — how each control is tested, including what a bypass attempt should see
## Open questions

Checklist

  • No local AEM users on Cloud Service; access comes through IMS product profiles.
  • Permissions are granted to groups, never to individual users.
  • Every service user is under system/cq:services, uses set principal ACL, and is scoped to one path with least privilege.
  • No loginAdministrative, and no service user with jcr:all on /.
  • Protected content has a CUG, and its caching strategy is explicit rather than assumed.
  • No personalised response is cached without a permission check.
  • Traffic filter rules were run in log-only mode before enforcing.
  • Purge keys are secrets, rotated, and absent from client-side code.
  • Security headers including CSP are set at the edge and verified on a real response.
  • Every secret is $[secret:…], and no credential is in Git or a content package.
  • CSRF protection is enabled, with tokens obtained properly rather than the filter disabled.

Anti-patterns

Anti-pattern Why it hurts Fix
Service user with jcr:all on / Any bug in that job becomes total repository compromise One user per job, set principal ACL, narrowest path, read unless write is required
set ACL with with path system/<proj> on Cloud Service Not the Cloud Service form; diverges from principal-based authorization with forced path system/cq:services/<proj> + set principal ACL
Disabling the CSRF filter to make a form submit Removes protection site-wide for one form Fetch a token from /libs/granite/csrf/token.json
Caching a page that renders user-specific content Serves one user's data to the next visitor Permission-sensitive caching, or public page + client-side authenticated fragment
Permissions granted per user Unauditable, and wrong the moment anyone moves team Groups mapped from IMS product profiles
Secrets as literals in .cfg.json Committed to Git forever and visible to anyone with repo access $[secret:…] with a Cloud Manager secret variable
Enforcing new WAF or rate-limit rules immediately The first thing blocked is usually your own integration Log-only first, read the would-be blocks, then enforce
Relying on dispatcher filters as the only defence One permissive rule exposes everything behind it Deny by default at dispatcher and correct ACLs in the repository

Go deeper

  • references/permissions-and-service-users.md — repoinit patterns, principal-based ACLs, CUG setup and permission-sensitive caching with its auth checker.
  • references/edge-and-headers.md — traffic filter and WAF rule shapes, rate limiting, purge key handling, the security header set, and a CSP rollout that survives client libraries.
  • Sibling skills: aem-dispatcher for filter and cache rule syntax; aem-code-review for code-level findings; aem-cloud-manager for where environment variables and secrets are set.

References

Deeper material the agent loads only when needed.