---
title: Authentik Access Groups
type: runbook
tags:
  - auth
aliases: []
related:
  - [[authentik-oidc-application]]
  - [[authentik]]
  - [[authentik-create-invitation]]
  - [[deploy-livesync]]
  - [[proxmox-onboard-node]]
updated: 2026-07-11
---
# Authentik Access Groups

Manage group-based application access via the [[authentik]] API. No web UI interaction required.

**[[authentik]] instance:** https://auth.echo6.co (edge2 CT 105, 100.64.0.36)

**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

```bash
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:

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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:

```bash
# 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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
# 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:

```bash
curl -s -H "Authorization: Bearer $AK_TOKEN" \
  "$AK_API/core/groups/$GROUP_PK/" \
  | jq '[.users_obj[].username]'
```

2. Verify the binding exists and is enabled:

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

3. 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*
