hashicorp / hashicorp/vault

OIDC login appends a duplicate entry to external group member_entity_ids on every login

Open
#32,057 1 comment 1 reaction 0 assignees View on GitHub
auth/oidc bug core/identity
Dominant language
Go
Stars
36.3k
Forks
4.8k
PR merge metrics
PR metrics pending

Description

## Summary

Every OIDC login appends the authenticating entity's ID to the `member_entity_ids` list of
each external identity group named in the user's groups claim — without checking whether the
entity is already a member. The list grows by one entry per login per group, unbounded, while
the set of actual members never changes.

The LDAP auth path does not exhibit this. Neither does the `identity/group` API path, which
deduplicates. Only the OIDC login path appears to skip the normalisation.

## Environment

| | |
|---|---|
| Vault server | 2.0.3, built 2026-06-17T12:49:01Z |
| Vault CLI | v2.0.0 (cf1ce4d) |
| Storage | Consul |
| HA | active |
| OIDC plugin | v2.0.3+builtin.vault |
| Auth methods in use | oidc, ldap, approle |

OIDC role config (relevant fields):

```
role_type oidc
groups_claim groups
user_claim preferred_username
oidc_scopes [openid profile email groups]
```

Group layout: eight external groups — four logical roles × two auth methods. Each external
group has exactly one group alias with the correct mount accessor; no duplicate aliases. The
four `*-ldap` and four `*-oidc` groups alias the same four IdP group names. Group and user
names below are anonymised; `group-d` is the one whose IdP group is **not** in this user's
claim, and serves as a control.

## Expected behaviour

After N logins by the same user, an external group whose alias matches a group in that user's
claim contains **one** entry for that user's entity in `member_entity_ids`.

## Actual behaviour

It contains **N** entries.

## Reproduction

Baseline for one entity (`user-a`):

```
GROUP raw entity-count modify_index
group-a-oidc 757 230 828
group-b-oidc 627 230 695
group-c-oidc 689 230 759
group-d-oidc 53 0 87 <- control: alias not in claim
group-a-ldap 7 1 28
group-b-ldap 4 1 18
group-c-ldap 4 1 25
group-d-ldap 3 0 8
```

Then **one** OIDC login (`vault login -method=oidc role=default`):

```
group-a-oidc 757 -> 758 (+1) entity 230 -> 231 (+1) modify_index 828 -> 829
group-b-oidc 627 -> 628 (+1) entity 230 -> 231 (+1) modify_index 695 -> 696
group-c-oidc 689 -> 690 (+1) entity 230 -> 231 (+1) modify_index 759 -> 760
group-d-oidc 53 -> 53 entity 0 -> 0 modify_index 87 -> 87
all four *-ldap unchanged
```

`len(set(member_entity_ids))` is unchanged throughout — no member is added, only a duplicate
appended.

Then **two** further OIDC logins, one with `role=default` and one with a second role
(`role-b`) that stamps an extra policy via `token_policies`:

```
group-a-oidc 757 -> 760 (+3 total) entity 230 -> 233 (+3)
group-b-oidc 627 -> 630 (+3 total) entity 230 -> 233 (+3)
group-c-oidc 689 -> 692 (+3 total) entity 230 -> 233 (+3)
group-d-oidc 53 -> 53 (unchanged)
all four *-ldap unchanged
```

Strictly linear: one appended entry per login, per claim-matched group.

## What this rules out

- **Not a read-time artefact.** Three consecutive reads return identical counts and an
unchanged `modify_index`. The duplicates are persisted.
- **Not driven by the role used.** The third login used `role-b`, yet `group-d-oidc` did not
grow — that group's alias is not in the user's claim. The update follows claim content only.
- **Not low LDAP usage masking the same bug.** The `*-ldap` groups have `modify_index` values
far above their member counts (e.g. `group-a-ldap`: 7 members, `modify_index` 28), so LDAP
logins do write to the group object repeatedly, and the list stays deduplicated. One LDAP
alias in this install last authenticated 2025-09-25 against a group created in 2022 and
still holds exactly one entry per entity.
- **Not a misconfiguration of group aliases.** Eight group aliases, one per external group,
each with the correct mount accessor; no duplicates, no cross-mount collisions.
- **Not a missing dedup in the identity API.** Writing five identical entity IDs to a
throwaway internal group via `identity/group` stores exactly one.

## Suspected cause

The identity group write path applies `strutil.RemoveDuplicates()` to `member_entity_ids`
(cf. #16088). The membership update performed during OIDC login evidently does not reach that
normalisation, whereas the LDAP equivalent does.

Not verified against source — inferred from the behavioural split between the two auth mounts
plus the API-path result above.

## Impact

Functionally benign: policy resolution is correct and the effective member set is right. Two
practical consequences:

1. **Unbounded object growth.** One group here holds 760 entries for 21 distinct members; the
most active entity contributes 233 of them, accumulated over roughly 29 months (~8/month).
The object grows for the lifetime of the install.
2. **`member_entity_ids` cannot be used to count members.** Any tooling, report or audit that
reads the list length gets a number inflated by up to ~157× in this install. This is
actively misleading: it caused a group of 4 people to be read as 627.

## Workaround

None found that is safe from outside. External groups reject manually supplied
`member_entity_ids`, so the deduplicating API path cannot be triggered for them, and writing
to the group object risks emptying membership until each affected user next logs in. Consumers
must deduplicate client-side.

Contributor guide

Open the contributing guide

Research direction

Start at the OIDC login membership update and compare it with the LDAP auth path and the identity/group API path, focusing on where member_entity_ids is normalized. Reproduce repeated logins for a claim-matched external group and verify that the list contains one entry per entity while an unmatched group remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.