echo6-docs/runbooks/authentik-access-groups.md
Matt Johnson e9231ac24a Migration: consolidate Echo6 docs to cortex with full infrastructure cleanup sync
- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup)
- Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing
- Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack
- Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure
- Removes 4 deprecated runbook duplicates (canonical versions live in projects/)
- Adds .gitignore for binary archives and editor temp files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 06:02:16 +00:00

10 KiB

Authentik Access Groups

Manage group-based application access via the Authentik API. No web UI interaction required.

Authentik instance: https://auth.echo6.co (Contabo, 100.64.0.1)

Key behavior: Users in authentik Admins (is_superuser=true) bypass ALL policy checks automatically. Group bindings only restrict non-superuser access.


How It Works

By default, any authenticated Authentik user can access any application. Adding a policy binding that ties a group to an application restricts that app to group members only (plus superusers).

  • One binding per group-application pair
  • An app can have multiple group bindings (policy_engine_mode=any means membership in ANY bound group grants access)
  • Apps with zero bindings remain open to all authenticated users
  • Superusers always have access regardless of bindings

Setup

AK_TOKEN="$(grep 'AUTHENTIK_API_TOKEN=' /home/zvx/projects/.ref/credentials | tail -1 | sed 's/.*=//' | tr -d '"')"
AK_API="https://auth.echo6.co/api/v3"

Verify:

curl -s -H "Authorization: Bearer $AK_TOKEN" "$AK_API/core/groups/?page_size=1" | jq '.pagination.count'

Must return a number. If 403, the token is invalid or expired.


Procedure A: Create a New Access Group

Inputs

GROUP_NAME=             # lowercase, hyphenated (e.g., "finance-users", "dev-users")

Convention: <category>-users (e.g., media-users, cloud-users, security-users).

Create the group

curl -s -X POST "$AK_API/core/groups/" \
  -H "Authorization: Bearer $AK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\": \"$GROUP_NAME\"}" | jq '{name: .name, pk: .pk}'

Store the returned pk as GROUP_PK.

Gate

Response must include a valid UUID pk. If it returns an error, the group name likely already exists.


Procedure B: Bind a Group to an Application

This restricts the application so only members of the bound group (and superusers) can access it.

Inputs

APP_SLUG=               # Application slug (e.g., "jellyfin", "nextcloud")
GROUP_PK=               # Group UUID from Procedure A or the reference table below

Look up the application PK

APP_PK=$(curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/applications/?slug=$APP_SLUG&superuser_full_list=true" \
  | jq -r '.results[0].pk')
echo "App PK: $APP_PK"

Must return a UUID. Use superuser_full_list=true because apps that already have bindings won't appear without it.

Check for existing bindings

curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/policies/bindings/?target=$APP_PK" \
  | jq '.results[] | {pk: .pk, group: .group_obj.name}'

Review output. If the desired group is already bound, skip creation.

Create the binding

curl -s -X POST "$AK_API/policies/bindings/" \
  -H "Authorization: Bearer $AK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"group\": \"$GROUP_PK\",
    \"target\": \"$APP_PK\",
    \"order\": 0,
    \"enabled\": true,
    \"negate\": false,
    \"timeout\": 30
  }" | jq '{pk: .pk, group: .group_obj.name, target: .target}'

Gate

Response must include a valid UUID pk. If it fails:

  • "target" invalid — the application PK is wrong
  • "group" invalid — the group PK is wrong

Procedure C: Add a User to a Group

Inputs

USERNAME=               # Authentik username (e.g., "jodie")
GROUP_PK=               # Group UUID

Look up the user PK

USER_PK=$(curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/users/?search=$USERNAME" \
  | jq -r '.results[0].pk')
echo "User PK: $USER_PK"

Get current group members

CURRENT_USERS=$(curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/groups/$GROUP_PK/" \
  | jq -r '[.users[]] | join(",")')
echo "Current user PKs: $CURRENT_USERS"

Add user to group

curl -s -X PATCH "$AK_API/core/groups/$GROUP_PK/" \
  -H "Authorization: Bearer $AK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"users\": [$CURRENT_USERS, $USER_PK]}" \
  | jq '{name: .name, users: [.users_obj[].username]}'

Gate

Response must list the user in users. The users field is a replace operation — always include existing user PKs to avoid removing them.


Procedure D: Remove a User from a Group

Same as Procedure C, but omit the user PK from the users array:

# Get current members, filter out the target user
NEW_USERS=$(curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/groups/$GROUP_PK/" \
  | jq -r "[.users[] | select(. != $USER_PK)] | join(\",\")")

curl -s -X PATCH "$AK_API/core/groups/$GROUP_PK/" \
  -H "Authorization: Bearer $AK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"users\": [$NEW_USERS]}" \
  | jq '{name: .name, users: [.users_obj[].username]}'

Procedure E: Remove a Group Binding from an Application

This re-opens the application to all authenticated users (if it was the only binding).

Find the binding PK

curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/policies/bindings/?target=$APP_PK" \
  | jq '.results[] | {binding_pk: .pk, group: .group_obj.name}'

Delete the binding

BINDING_PK=             # From the output above
curl -s -X DELETE -w "%{http_code}" \
  -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/policies/bindings/$BINDING_PK/"

Must return 204.


Procedure F: Rename a Group

OLD_GROUP_PK=           # UUID of the group to rename
NEW_NAME=               # New name (e.g., "media-users")

curl -s -X PATCH "$AK_API/core/groups/$OLD_GROUP_PK/" \
  -H "Authorization: Bearer $AK_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\": \"$NEW_NAME\"}" \
  | jq '{name: .name, pk: .pk}'

Renaming propagates to all existing bindings automatically — no need to recreate bindings.


Verification

List all groups and members

curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/groups/?page_size=50" \
  | jq '.results[] | {name: .name, pk: .pk, superuser: .is_superuser, users: [.users_obj[].username]}'

List all application bindings

curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/applications/?superuser_full_list=true&page_size=50" \
  | jq -r '.results[] | .slug' | while read slug; do
    echo "--- $slug ---"
    APP_PK=$(curl -s -H "Authorization: Bearer $AK_TOKEN" \
      "$AK_API/core/applications/?slug=$slug&superuser_full_list=true" \
      | jq -r '.results[0].pk')
    curl -s -H "Authorization: Bearer $AK_TOKEN" \
      "$AK_API/policies/bindings/?target=$APP_PK" \
      | jq -r 'if .results | length == 0 then "  (open to all)" else .results[] | "  \(.group_obj.name)" end'
done

Check what a specific user can see

# This shows apps visible to the API token owner without superuser bypass
curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/applications/?superuser_full_list=false" \
  | jq '[.results[].name]'

For a non-superuser, this returns only apps they have group access to plus unbound apps.


Troubleshooting

User gets "access denied" after binding was added

  1. Verify the user is in the correct group:
curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/groups/$GROUP_PK/" \
  | jq '[.users_obj[].username]'
  1. Verify the binding exists and is enabled:
curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/policies/bindings/?target=$APP_PK" \
  | jq '.results[] | {group: .group_obj.name, enabled: .enabled, negate: .negate}'
  1. Check that negate is false — if true, the binding denies access instead of granting it.

Superuser can't see all apps in the UI

The Authentik user library page uses superuser_full_list=false by default. Superusers always have SSO access to all apps, but the library page only shows apps the user is explicitly authorized for. This is cosmetic — direct URL access still works.

App disappeared from user's library after adding first binding

Expected behavior. Before any bindings exist, the app is open to everyone. The moment you add the first group binding, only that group's members (and superusers) see it. Make sure all intended users are in the group before binding.


Quick Reference: Current State

Groups

Group PK Members
authentik Admins 9944e153-f860-4443-81d1-ae544f611806 akadmin, matt (superuser)
media-users 0820b2b8-6c54-4c20-9a0a-872820e6d9ea jodie
communication-users 31bce176-cd86-4aea-8db3-a57e03d5c2d1
security-users f345a043-c2a4-4906-a43b-9860eae86ee1
productivity-users 698d80c7-7c29-43cd-b5d4-9eb24c85a6cc
cloud-users db3cbf5d-8057-4e33-8e8d-95bfdb35fbac
proxmox_admins d85a868d-7d1e-4585-92a8-b8bb86771b53 akadmin, matt
proxmox_users cf26703a-a824-47dd-9550-30b848a8ce5f

Application Bindings

Application Slug Group Binding PK
Jellyfin jellyfin media-users 31515ffc-f937-442f-9813-263e68247687
Jellyseer jellyseer media-users (existing)
PeerTube peertube media-users c0f79fd3-9270-49f6-8457-42affc96c50a
Mailcow mailcow communication-users 5a8f92de-81d5-4cf6-9273-7093de0f568d
Vaultwarden vaultwarden security-users a39e9d0c-237d-4a62-976e-b6c74ee31629
Forgejo forgejo productivity-users 49953de3-af0b-4b1b-954d-70684d127445
Nextcloud nextcloud cloud-users 6ac8ccfc-7ca3-4288-a281-b78a1c675e57
Immich immich cloud-users 4fdf2887-e94f-4c24-81d2-d8cd4587ef38

Unbound Applications (open to all authenticated users)

Application Slug Reason
Headplane headplane Admin tool — superuser access only needed
Headscale VPN headscale Admin tool — superuser access only needed
Proxmox VE proxmox Admin tool — superuser access only needed
WATCHTOWER watchtower Admin tool — superuser access only needed

Last updated: 2026-02-14 — Initial creation with 5 access groups and 8 application bindings