Free SKILL.md scraped from GitHub. Clone the repo or copy the file directly into your Claude Code skills directory.
npx versuz@latest install seb155-atlas-plugin-skills-authentik-auditgit clone https://github.com/seb155/atlas-plugin.gitcp atlas-plugin/SKILL.MD ~/.claude/skills/seb155-atlas-plugin-skills-authentik-audit/SKILL.md---
name: authentik-audit
description: "Authentik OIDC provider audit — scope sweep, policy bindings, property mappings. Use when 'authentik audit', 'check OIDC', 'sso audit', 'provider scopes', or after onboarding new external user."
mode: [ops]
effort: medium
version: 1.0.0
tier: [admin]
---
# Authentik Audit — OIDC Scope, Policy Bindings, Property Mappings
> Pain-point ROI skill (W6.2): catches three classes of silent SSO failures
> learned the hard way during 2026-04-29/30 Jeremy + Charles onboarding —
> a provider missing `profile` scope worked for legacy users but broke
> greenfield ones, and 19 apps without policy bindings were open to ALL
> authenticated users.
>
> Target: 15-20 min/week of preventive sweep instead of fire-fighting outages.
## When to Use
- User says: "authentik audit", "check OIDC", "sso audit", "provider scopes",
"policy bindings audit", "are SSO apps locked down"
- After onboarding a new external user (catches greenfield-only breaks)
- After creating a new Authentik application or provider
- After modifying a `default OAuth Mapping` property mapping
- Weekly preventive cron (paired with `atlas-routines`)
- Before granting an external user (contractor, candidate) access
## Why It Exists (incidents)
| Date | Incident | Lesson |
|------|----------|--------|
| 2026-04-29 | 19 Authentik apps had `policy_bindings: []` → any auth user passed SSO | `feedback_authentik_policy_bindings_default.md` |
| 2026-04-29 | HA + 17 providers w/ `property_mappings: []` exposed only `openid`, breaking greenfield users silently | `lesson_authentik_provider_zero_mappings_silent_oidc_fail.md` |
| 2026-04-30 | Forgejo provider had partial mappings (missing `profile`) — Jeremy login failed, Seb worked | same lesson, greenfield variant |
Documentation alone (rules, memories) did not prevent these. This skill
makes detection mechanical and re-runnable.
## Three Audit Dimensions
### Dimension 1 — Provider Scope Sweep
**Goal**: every OIDC provider must expose the 4 standard scopes:
`openid`, `profile`, `email`, `groups`.
**Failure mode**: provider with `property_mappings: []` exposes ONLY
`openid` (no claims) — login appears to succeed at Authentik but the
upstream service either rejects the user (no email/username) or treats
them as anonymous. Greenfield-user-only failure for partial mappings.
**Standard mappings** (must be registered + assigned):
- `authentik default OAuth Mapping: OpenID 'profile'`
- `authentik default OAuth Mapping: OpenID 'email'`
- `authentik default OAuth Mapping: OpenID 'groups'`
`openid` is implicit — not a separate mapping, but always present.
**API**: `GET /api/v3/providers/oauth2/{id}/` → inspect
`property_mappings` array against the 3-mapping baseline.
### Dimension 2 — Policy Bindings Audit
**Goal**: every "sensitive" application must have at least one
`policy_binding` that gates access. Apps with `policy_bindings: []` are
**open to ALL authenticated Authentik users**, including external/guest
accounts.
**Sensitive class** (audit-default): vault.*, proxmox.*, coder.*,
forgejo.*, woodpecker.*, traefik.*, infisical.*, grafana.*, paperless.*,
immich.*, ragflow.*, netbird.*, anything tagged `internal=true` in the
app metadata.
**Standard binding** (per `feedback_authentik_policy_bindings_default.md`):
expression policy `deny-external-devs` with `negate=true` (allow only
non-external users).
**API**: `GET /api/v3/core/applications/{slug}/` → inspect
`policy_bindings` array length + bound policy names.
### Dimension 3 — Property Mappings Completeness
**Goal**: confirm the 3 standard `default OAuth Mapping` property
mappings are registered globally and have non-empty expression bodies.
A missing or stub mapping cascades silently across every provider that
references it.
**API**: `GET /api/v3/propertymappings/provider/scope/` → filter
`managed=goauthentik.io/providers/oauth2/scope-{profile,email,groups}`.
## Authentik API
| Setting | Value |
|---------|-------|
| Base URL | `https://authentik.s-gagnon.com/api/v3/` |
| Token | `AUTHENTIK_TOKEN` from `~/.env` (load via `secret-manager` skill) |
| Auth header | `Authorization: Bearer ${AUTHENTIK_TOKEN}` |
### Endpoints used
| Method + Path | Purpose |
|---------------|---------|
| `GET /providers/oauth2/` | List all OIDC providers (paginated) |
| `GET /providers/oauth2/{id}/` | Provider detail incl. `property_mappings` |
| `GET /core/applications/` | List all applications |
| `GET /core/applications/{slug}/` | Application detail incl. `policy_bindings` |
| `GET /policies/bindings/?target={app_uuid}` | Bindings filtered to an app |
| `GET /propertymappings/provider/scope/` | Scope mappings registry |
### Token loading (reuse `secret-manager`)
```bash
# Loaded via secret-manager skill; do not duplicate logic here
source ~/.env # fallback path
: "${AUTHENTIK_TOKEN:?AUTHENTIK_TOKEN missing — run secret-manager skill}"
AUTHENTIK_API="https://authentik.s-gagnon.com/api/v3"
```
## CLI Surface
| Command | Behavior |
|---------|----------|
| `atlas authentik-audit` | Full sweep — all 3 dimensions, ASCII tables, exit 0/1 |
| `atlas authentik-audit --providers-only` | Dimension 1 only (scope sweep) |
| `atlas authentik-audit --policy-bindings-only` | Dimension 2 only (policy bindings) |
| `atlas authentik-audit --mappings-only` | Dimension 3 only (mapping registry) |
| `atlas authentik-audit --report` | Full sweep + write markdown report to `memory/authentik-audit-YYYY-MM-DD.md` |
| `atlas authentik-audit --json` | Machine-readable output (for routines/cron) |
Exit code: `0` if PASS on every dimension, `1` if any FAIL (≥1 gap).
## Output Format
### Dimension 1 — Provider Scope Sweep
```
PROVIDER SCOPE SWEEP — 19 providers
────────────────────────────────────────────────────────────────────
PROVIDER profile email groups STATUS
forgejo-provider ✓ ✓ ✓ PASS
ha-provider ✓ ✓ ✗ FAIL (missing groups)
vault-provider ✗ ✗ ✗ FAIL (zero mappings)
...
────────────────────────────────────────────────────────────────────
RESULT: 16 PASS, 3 FAIL
```
### Dimension 2 — Policy Bindings
```
POLICY BINDINGS AUDIT — 22 sensitive apps
────────────────────────────────────────────────────────────────────
APP BINDINGS POLICY STATUS
vault 1 deny-external-devs PASS
proxmox 0 (none) FAIL
coder 1 deny-external-devs PASS
...
────────────────────────────────────────────────────────────────────
RESULT: 18 PASS, 4 FAIL — 4 apps OPEN to all auth users
```
### Dimension 3 — Mapping Registry
```
PROPERTY MAPPINGS REGISTRY
────────────────────────────────────────────────────────────────────
MAPPING EXPR LEN STATUS
authentik default OAuth Mapping: OpenID 'profile' 132 PASS
authentik default OAuth Mapping: OpenID 'email' 87 PASS
authentik default OAuth Mapping: OpenID 'groups' 0 FAIL (empty)
────────────────────────────────────────────────────────────────────
RESULT: 2 PASS, 1 FAIL
```
## Remediation Snippets
When a FAIL is reported, the skill prints copy-pasteable curl commands.
### Fix a provider missing `groups`
```bash
# Get current mappings
PROVIDER_ID=3
CURRENT=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_API/providers/oauth2/$PROVIDER_ID/" | jq '.property_mappings')
# Find groups mapping UUID
GROUPS_UUID=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_API/propertymappings/provider/scope/?managed=goauthentik.io/providers/oauth2/scope-groups" \
| jq -r '.results[0].pk')
# PATCH provider with merged list
NEW_MAPPINGS=$(echo "$CURRENT" | jq --arg g "$GROUPS_UUID" '. + [$g] | unique')
curl -X PATCH -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"property_mappings\": $NEW_MAPPINGS}" \
"$AUTHENTIK_API/providers/oauth2/$PROVIDER_ID/"
```
### Fix an app missing policy binding
```bash
APP_SLUG="proxmox"
APP_UUID=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_API/core/applications/$APP_SLUG/" | jq -r '.pk')
POLICY_UUID=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_API/policies/all/?name=deny-external-devs" \
| jq -r '.results[0].pk')
curl -X POST -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"target\":\"$APP_UUID\",\"policy\":\"$POLICY_UUID\",\"negate\":true,\"order\":0,\"enabled\":true}" \
"$AUTHENTIK_API/policies/bindings/"
```
### Fix an empty `groups` property mapping
This is a manual fix in the Authentik UI under
**Customisation → Property Mappings**. The default expression body for
`groups` is:
```python
return [group.name for group in request.user.ak_groups.all()]
```
## Routine Integration (weekly preventive)
Pair with `atlas-routines` skill to schedule a weekly run:
```bash
atlas atlas-routines create authentik-audit-weekly \
--schedule "0 9 * * 1" \
--command "atlas authentik-audit --report"
# Mondays 09:00 EDT — output appended to memory/
```
If FAILs are detected, the routine should open a Forgejo issue tagged
`authentik-audit-drift` with the report excerpt.
## Constraints + Safety
- **READ-ONLY by default** — the skill never PATCHes or POSTs without
explicit `--fix` flag (not implemented in v1.0; remediation is
copy-paste only).
- Token is read from `~/.env` via `secret-manager`; never echoed,
never logged. Failure to load → skill exits with clear error.
- Network: API base must respond within 5s; otherwise skill bails.
- Multi-tenant: scoped to `authentik.s-gagnon.com` only; no cross-tenant
enumeration.
## Verification Test (one-shot)
After install, validate the skill end-to-end:
```bash
# Smoke: list providers, expect ≥1
curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
"$AUTHENTIK_API/providers/oauth2/" | jq '.pagination.count'
# Run skill in dimension-1-only mode
atlas authentik-audit --providers-only --json | jq '.providers | length'
```
## Related
- Memory: `lesson_authentik_provider_zero_mappings_silent_oidc_fail.md`
- Memory: `feedback_authentik_policy_bindings_default.md`
- Memory: `lesson_forgejo_oidc_local_user_pre_creation_blocks_link.md`
- Memory: `lesson_authentik_recovery_link_burn_by_email_scanners.md`
- Plan: `.blueprint/plans/authentik-provider-mappings-audit-2026-04-30.md`
- Skill: `secret-manager` (token loading)
- Skill: `atlas-routines` (weekly cron integration)
- Skill: `infra-health` (sibling — endpoint reachability vs SSO config)
- Companion rule: `.claude/rules/no-corpo-leak.md` (no real user data in audit reports committed to repos)