> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/security/password-policy.md).

# Password policy

Build a password policy with real parameter names and shipped defaults, understand how one policy is selected out of many, and prove the change with a positive and a negative test.

## What this is, and when you need it

A password policy is a row in `POLICY` plus a set of `POLICY_ATTRIBUTE` rows, each pointing at one of the **59** parameter definitions in `POLICY_DEF_PARAM`. It decides what a password may contain, how long it lives, how many previous values are remembered, and what happens after an administrator resets it. It also carries the forgot-password and security-question settings, which is why [Configure password recovery and account unlock](/administration-guide/security/password-recovery.md) starts here too.

The complete parameter table — every ID, name, description, validation handler class and the value policy `4000` ships with — is [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md); this page does not repeat it. What it adds sits after the procedure: the two-table data model, the three parameter groups, how one policy is chosen out of five candidate sources, and which composition rules interact. The worked example states every value it needs, so you can start without reading any of it.

Lockout is **not** here. `FAILED_AUTH_COUNT`, `AUTO_UNLOCK_TIME` and `TOKEN_LIFE` live on the *authentication* policy, `4001`.

{% hint style="warning" %}
**`PASS_COMPROMISED_CHECK` ships ENABLED on policy `4000`.** The attribute ships with `VALUE1='true'` and `REQUIRED='Y'`, which is everything the check needs, and no release since has changed either row. Confirm it on **Policy → Password Policy → 4000**, parameter **Check if password has been compromised**. Documentation telling you to switch it on describes an older release; what you may need to do is switch it *off*. See [Compromised password detection](/administration-guide/security/compromised-passwords.md).
{% endhint %}

## At a glance

|                          |                                                                                                                                                                                                                                                                                                           |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | A second, higher-priority password policy that demands 14 to 64 characters and all four character classes, proven by one password it accepts and four it refuses, with the shipped policy `4000` left untouched as the recovery floor                                                                     |
| **Where**                | **Policy → Password Policy**, at `/webconsole/editPolicy?id=<POLICY_ID>` — `New` on the same screen creates one. `/webconsole/passwordPolicy` still works but is deprecated as of 4.2.2                                                                                                                   |
| **You need first**       | The strictest length, character-set and history rule among the managed systems that will receive synchronized password changes; the priorities already in use; a test organization containing one disposable identity and nobody else; and the passing and failing password values chosen before you save |
| **Takes**                | 45 to 90 minutes, including a 72-second wait for `org.openiam.policy.sweep.time` after each association change                                                                                                                                                                                            |
| **Reversible**           | Yes: clear **Active** on the new policy and resolution falls back to what it was before. Password history already written for the test identity is not removed, and `4000` is only the floor while you leave it alone                                                                                     |

## Before you begin

Two of the prerequisites above need more than a line. The strictest rule among the managed systems that will receive synchronized password changes is the real ceiling: OpenIAM cannot make a target accept a password the target rejects. And you need **two** disposable identities, not one — one that receives only the new policy, one that also receives a lower-priority policy, because proving resolution needs both.

## Worked example: tighten composition on one policy and prove it

The objective: a workforce policy that requires 14 to 64 characters, at least one uppercase, one lowercase, one digit and one symbol, and forbids reuse of the last twelve passwords — while leaving the shipped `4000` untouched as the recovery floor.

{% stepper %}
{% step %}

### Create the policy, inactive, at a known priority

**Policy → Password Policy → New**. Name it `Workforce password policy`, describe the population and the owner, and set **Priority** to `20`. Leave `Active` off — an inactive policy is a checkpoint you can inspect. An inactive policy genuinely does nothing — it is skipped wherever policies are looked up, including help-desk question lookup, so it will not surprise you later.

Record the priorities already in use before you pick `20`. Two overlapping populations at the same priority make resolution a coin toss: equal priorities leave the sort order undisturbed, so the winner depends on the order the candidate list happened to be built in, which depends on the user's entitlements. There is nothing you can inspect afterwards that will tell you which one won.
{% endstep %}

{% step %}

### Set the composition parameters as one coherent change

| Parameter           | `VALUE1` | `VALUE2`  |
| ------------------- | -------- | --------- |
| `PWD_LEN`           | `14`     | `64`      |
| `UPPERCASE_CHARS`   | `1`      | *(empty)* |
| `LOWERCASE_CHARS`   | `1`      | *(empty)* |
| `NUMERIC_CHARS`     | `1`      | *(empty)* |
| `NON_ALPHA_CHARS`   | `1`      | *(empty)* |
| `PWD_HIST_VER`      | `12`     | —         |
| `MIN_CHARS_CHANGED` | `4`      | —         |
| `PWD_LOGIN`         | `true`   | —         |
| `PWD_NAME`          | `true`   | —         |

Check the arithmetic before you save: the four minima sum to 4, comfortably inside a minimum length of 14. Minima that sum above `PWD_LEN.VALUE1` make every password impossible.

`MIN_CHARS_CHANGED` is dropped from `10` to `4` deliberately. At a 14-character minimum, ten differing characters is a near-total rewrite each cycle, which drives people to append counters.
{% endstep %}

{% step %}

### Set the change and expiration behaviour

`CHNG_PSWD_ON_RESET` = `1`, `CHNG_PSWD_ON_1ST_LOGIN` = `true`, `PWD_EXPIRATION` = `365`, `PWD_EXP_WARN` = `14`, `PWD_EXP_GRACE` = `0`, `PASSWORD_NEVER_EXPIRE` = `false`. Leave `PASS_COMPROMISED_CHECK` at `true` unless you have decided otherwise on [Compromised password detection](/administration-guide/security/compromised-passwords.md).
{% endstep %}

{% step %}

### Save, reopen, and read the values back

`Save` proves acceptance, not persistence of what you meant. Reopen the policy and confirm `PWD_LEN` shows `14` and `64`, not `14` and blank. Then confirm at the database if you can:

{% code overflow="wrap" %}

```sql
SELECT d.NAME, a.VALUE1, a.VALUE2, a.REQUIRED
FROM   POLICY_ATTRIBUTE a JOIN POLICY_DEF_PARAM d ON a.DEF_PARAM_ID = d.DEF_PARAM_ID
WHERE  a.POLICY_ID = '<YOUR_POLICY_ID>'
ORDER  BY d.PARAM_GROUP, d.NAME;
```

{% endcode %}
{% endstep %}

{% step %}

### Associate it with one test organization, then activate

Attach the policy to a test organization that contains `<TEST_USER>` and nobody else. Wait out `org.openiam.policy.sweep.time` — `72000` ms — before testing.
{% endstep %}

{% step %}

### Prove resolution before testing composition

Confirm the *right* policy is being applied, or your composition test tells you nothing. Change `<TEST_USER>`'s password to something that policy `4000` would accept and yours would not — a 10-character value with a digit. It must be **rejected**. If it is accepted, resolution picked `4000`: check the priority, the `Active` flag, and whether a higher-priority policy arrives through a role, a group, or the managed system.
{% endstep %}

{% step %}

### The positive test

`Correct-horse-Battery9!` — 23 characters, upper, lower, digit, symbol, no angle brackets, not the login ID, not a name. Expect acceptance, and expect the compromised-password notification **not** to arrive, since this value is not in the breach corpus.
{% endstep %}

{% step %}

### The negative test that finds the mistake

Run all four, and expect a rejection each time:

| Value                                                    | Rule it must trip                                        |
| -------------------------------------------------------- | -------------------------------------------------------- |
| `Short1!aaaaa`                                           | `PWD_LEN` minimum — 12 characters, under 14              |
| `correct-horse-battery9!`                                | `UPPERCASE_CHARS` minimum                                |
| `Correct-horse-Battery9!` again, after the positive test | `PWD_HIST_VER` — the previous value is among the last 12 |
| `<TEST_USER>-Passphrase9!` containing the login ID       | `PWD_LOGIN`                                              |

A policy that accepts the good password proves nothing until it also refuses the bad ones. If any rejection does not happen, the parameter did not persist or the policy did not resolve — recheck step 4 and step 6 rather than adding more rules.
{% endstep %}

{% step %}

### Then test one managed system

Change the password again and confirm the connector reply and the actual account in `<MANAGED_SYSTEM_ID>`. OpenIAM accepting a value says nothing about the target. A password that passes here and fails in Active Directory is the case that must be visible in [Diagnose a failed operation](/administration-guide/operations/diagnose-a-failed-operation.md), not silent.
{% endstep %}
{% endstepper %}

## Verification

| What to check                     | How                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The values persisted              | Reopen the policy, or the `POLICY_ATTRIBUTE` query above                                                                                                                                                                                                                                                                                                                                |
| The right policy resolved         | The negative test in step 6. There is no UI that prints the resolved policy — the observable proof is a value `4000` would accept being refused                                                                                                                                                                                                                                         |
| The change is live in the cache   | `org.openiam.policy.sweep.time` = `72000` ms                                                                                                                                                                                                                                                                                                                                            |
| A custom resolver actually loaded | Absence of *"can't create custom policy resolver"* in the ESB log. The fallback is silent apart from that line                                                                                                                                                                                                                                                                          |
| Audit records exist               | `CREATE_PASSWORD_POLICY`, `MODIFY_PASSWORD_POLICY`, `DELETE_PASSWORD_POLICY` for the policy itself; `CHANGE_PASSWORD`, `CHANGE_PASSWORD_FORM_SUBMIT`, `CHANGE_PASSWORD_EXTENDED` and `VALIDATE_CURRENT_PASSWORD` for the tests; `SAVE_PASSWORD_DICTIONARY` if you upload a deny list. See [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md) |
| The target accepted it            | `RESET_PASSWORD_CONNECTOR_RESPONSE` and the account in the managed system                                                                                                                                                                                                                                                                                                               |

## When it goes wrong

| Symptom                                                       | Cause                                                                                                                                 | Fix                                                                                                                       |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| A password you expect to pass is refused with no obvious rule | `PWD_LEN.VALUE2` — policy `4000` ships a **maximum** of 12                                                                            | Set `VALUE2`, or clear it for no ceiling                                                                                  |
| A password change is refused every cycle                      | `MIN_CHARS_CHANGED` is `10` on `4000` against a maximum length of `12`                                                                | Lower `MIN_CHARS_CHANGED`, or raise `PWD_LEN.VALUE2`                                                                      |
| `<` or `>` rejected                                           | `REJECT_CHARS_IN_PSWD` ships `<>` on policy `4000`                                                                                    | Clear it if your targets accept those characters                                                                          |
| A different policy applies than you configured                | Resolution collects role, group, organization, provider **and** `MNG_SYS.POLICY_ID` candidates and takes the highest `PRIORITY` whole | List every candidate. Do not expect the strongest field from each to be merged                                            |
| Two policies at the same priority                             | Equal priorities do not break the tie, so the winner follows candidate-list order                                                     | Give overlapping populations distinct priorities                                                                          |
| Policy has no effect at all                                   | The policy is not active, and an inactive policy is skipped everywhere policies are looked up                                         | Activate it                                                                                                               |
| Custom resolver script has no effect                          | The script failed to load and fell back silently, or the `policyResolver` cache has not turned over                                   | Check the ESB log for *"can't create custom policy resolver"*; confirm your class extends `DefaultPasswordPolicyResolver` |
| Reset links expire after three days and no field says so      | `NUM_DAYS_FRGT_PWD_TOKEN_VALID` is empty, and OpenIAM falls back to `3` rather than to zero                                           | Set the parameter explicitly                                                                                              |
| Expiration fields greyed out                                  | `PASSWORD_NEVER_EXPIRE` is `true`                                                                                                     | Expected behaviour                                                                                                        |
| First-login change never happens                              | The identity is not in `PENDING_INITIAL_LOGIN`                                                                                        | Fix the creating process, not the policy                                                                                  |
| Users get a compromised-password email you did not enable     | `PASS_COMPROMISED_CHECK` ships `true` / `REQUIRED='Y'` on `4000`                                                                      | See [Compromised password detection](/administration-guide/security/compromised-passwords.md)                             |
| You need a rule that is not in the 59                         | Parameter handlers are compiled into the product; there is no password regex field anywhere                                           | A product change — **Developer Guide** — or approximate with a dictionary and the minima                                  |

## Rollback

Restore the recorded `VALUE1` / `VALUE2` for each parameter you changed and re-run the positive and negative tests. A policy change is reversible; the password changes it caused are not — a user who has already rotated to a compliant value keeps it. If you have locked a population out of changing their password, the fastest reversal is to deactivate your policy so resolution falls back to the next-highest priority, and ultimately to `4000`. Do not delete a policy that is still associated with a role, group, organization, provider or managed system.

## The policy model: definition rows and attribute rows

Two tables, and the distinction matters because only one of them is per-policy.

| Table              | Row means                            | Key columns                                                                                         |
| ------------------ | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `POLICY_DEF_PARAM` | A parameter *exists* in this product | `DEF_PARAM_ID`, `NAME`, `DESCRIPTION`, `OPERATION`, `VALUE1`, `PARAM_GROUP`, `POLICY_PARAM_HANDLER` |
| `POLICY_ATTRIBUTE` | This policy *sets* that parameter    | `POLICY_ATTR_ID`, `DEF_PARAM_ID`, `POLICY_ID`, `VALUE1`, `VALUE2`, `RULE_TEXT`, `REQUIRED`          |

Three consequences you will meet:

**A parameter with no `POLICY_ATTRIBUTE` row for your policy is not "0" — it is absent.** Absence usually means "rule not applied", but not always: see the forgot-password token below, where absence means a fixed three days.

**`REQUIRED` is on the attribute row, not the definition.** For `PASS_COMPROMISED_CHECK`, OpenIAM requires `REQUIRED='Y'` *and* `VALUE1='true'` before the check runs, so clearing `REQUIRED` disables it as surely as setting the value to `false`. The Webconsole checkbox writes `VALUE1` only, which is why the `POLICY_ATTRIBUTE` query in [Compromised password detection](/administration-guide/security/compromised-passwords.md) reports both columns.

**Ranges use both value columns.** `PWD_LEN` on policy `4000` is `VALUE1='8'`, `VALUE2='12'` — a minimum *and a maximum*. So are `ALPHA_CHARS`, `NUMERIC_CHARS`, `UPPERCASE_CHARS`, `LOWERCASE_CHARS` and `NON_ALPHA_CHARS`. A single number in the Webconsole field is a minimum with no ceiling.

## The three parameter groups

`PARAM_GROUP` is a plain string column, and only three of its values are treated as known groups — `PSWD_COMPOSITION`, `FORGET_PSWD` and `PSWD_CHANGE_RULE`:

| Group              | Count | What it holds                                                                                                          |
| ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------- |
| `PSWD_COMPOSITION` | 20    | Length, character classes, history, dictionary and reject rules — the things a candidate password is validated against |
| `FORGET_PSWD`      | 11    | OTP lifetime and resend limits, lock time, token validity, security-question counts, help-desk question counts         |
| `PSWD_CHANGE_RULE` | 4     | `CHNG_PSWD_ON_1ST_LOGIN`, `MAX_RETRIES_VALIDATE_PASSWORD`, `PASSWORD_NEVER_EXPIRE`, `PASS_COMPROMISED_CHECK`           |

Three further group strings occur in the shipped data without being one of those three — `MISC` (1 parameter, `PWD_EXP_WARN`) and `SELFSERVE` (2, `QUEST_LIST` and `QUEST_SRC`) — plus 21 parameters with no group at all, which is where `PWD_EXPIRATION`, `PWD_EXP_GRACE`, `CHNG_PSWD_ON_RESET` and every authentication-policy parameter sit. `SELECT PARAM_GROUP, COUNT(*) FROM POLICY_DEF_PARAM GROUP BY PARAM_GROUP` gives you the current distribution. Do not expect the Webconsole tabs to map one-to-one onto `PARAM_GROUP`; they do not. See [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md) for the full grouping.

## How a policy is selected for a user

This is the part that surprises people. OpenIAM gathers candidates from **five** places, all of them at once:

| Candidate                                                                                       | The link it follows                               |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Every policy linked to any role the user holds                                                  | `ROLE.POLICY_ID`, for every role                  |
| Every policy linked to any group                                                                | The group's policy, for every group               |
| Every policy linked to any organization                                                         | The organization's policy, for every organization |
| The policy on the matched URI pattern's — else the content provider's — authentication provider | `AUTH_PROVIDER.POLICY_ID`                         |
| The managed system's own policy                                                                 | `MNG_SYS.POLICY_ID`                               |

Role, group and organization lookups are batched at 1,000 IDs per query, which keeps them under the SQL Server 2,100-parameter and Oracle 1,000-parameter limits. A heavily entitled user therefore contributes many candidates, and every one of them is a full policy.

The candidate list then goes to a resolver, which picks exactly one. The shipped resolver sorts by **`PRIORITY` descending** and takes the first; a policy with no priority sorts last. It does **not** merge. **The highest numeric priority wins as a whole policy**, so a priority-20 privileged-user policy that forgot to set `PWD_HIST_VER` does not inherit history from the priority-10 default.

If the candidate list is empty, or the resolver picks nothing, OpenIAM falls back to the password policy of the provider named by `org.openiam.default.auth.provider.id`, which ships as `OPENIAM_DEFAULT_AUTH` with `POLICY_ID='4000'`. **Policy `4000` is the floor.**

There is no screen that prints the resolved policy, so none of this is confirmable by inspection. The observable proof is behavioural, and step six of the worked example is how you get it: set a value that one policy would accept and the other would refuse, and see which answer you get.

{% hint style="info" %}
**The managed-system link is the one people forget.** `MNG_SYS.POLICY_ID` is a candidate on every password change scoped to that managed system, and it competes on priority with the user's role, group and organization policies. If an Active Directory managed system carries a strict policy, that policy can win for a user whose organization policy is laxer — which is usually what you want, and never what you expected.
{% endhint %}

### Replacing the resolver

`org.openiam.password.policy.resolver.script` (category `Authentication`, type `Groovy`) ships **empty**, which means the shipped priority-based resolver is used. Set it to a script ID and OpenIAM uses your script instead — but if the script will not load for any reason, it records *"can't create custom policy resolver"* in the ESB log and falls back to the shipped resolver silently. That log line is the only signal, so check for it after every change: a resolver that never loaded looks exactly like a resolver that decided to behave like the default. The resolved resolver is cached under the name `policyResolver`, so a script change waits for that cache to turn over.

The contract is one method. Override `resolve()`, take the search context and the candidate list, and return the one policy that wins:

{% code overflow="wrap" expandable="true" %}

```groovy
import org.openiam.esb.core.pswd.service.impl.DefaultPasswordPolicyResolver
import org.openiam.esb.domain.PolicyEntity
import org.openiam.idm.srvc.policy.dto.PasswordPolicyAssocSearchBean

class MostRestrictiveLengthResolver extends DefaultPasswordPolicyResolver {

    @Override
    PolicyEntity resolve(final PasswordPolicyAssocSearchBean searchBean, final List<PolicyEntity> policies) {
        if (policies == null || policies.isEmpty()) {
            return null   // null means the global password policy (4000) is used
        }
        return policies.sort { PolicyEntity p ->
            def attr = p.getPolicyAttributes()?.find { it.definition?.name == 'PWD_LEN' }
            -(attr?.value1 as Integer ?: 0)
        }.first()
    }
}
```

{% endcode %}

Returning `null` is legal and means "use the global policy", which is `4000`. Register the script per [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md) and put its ID in the property.

## The composition rules that actually interact

Every rule is a Java handler class named in `POLICY_DEF_PARAM.POLICY_PARAM_HANDLER`. Four interactions cause most of the "why was my good password rejected" tickets, with the shipped policy-`4000` values:

| Parameter                 | Policy `4000` | What it does that surprises people                                                                                                                                 |
| ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PWD_LEN`                 | `8` – `12`    | A **maximum** of 12 ships. Longer passphrases are rejected until you raise `VALUE2`                                                                                |
| `NUMERIC_CHARS`           | `1`           | The only character-class minimum that ships. Uppercase, lowercase, alpha and non-alphanumeric all ship empty                                                       |
| `PWD_HIST_VER`            | `6`           | Six previous values are remembered and refused                                                                                                                     |
| `MIN_CHARS_CHANGED`       | `10`          | **Ten characters must differ from the previous password**, inside a maximum length of 12. With `PWD_LEN` at 8-12 this is close to "the whole password must change" |
| `PASSWORD_CHANGE_ALLOWED` | `1`           | Limits how many times the password may be changed in the window                                                                                                    |
| `REJECT_CHARS_IN_PSWD`    | `<>`          | Angle brackets are **already** rejected. Not empty                                                                                                                 |
| `CHNG_PSWD_ON_1ST_LOGIN`  | `true`        | Enforced for an identity in `PENDING_INITIAL_LOGIN`                                                                                                                |
| `CHNG_PSWD_ON_RESET`      | `1`           | Read as a string; `"false"` is the only value that turns it off                                                                                                    |

Read the shipped values on your own system with the `POLICY_ATTRIBUTE` query in the worked example — an upgraded deployment may already have been tuned.

The pair to look at hardest is `PWD_LEN` and `MIN_CHARS_CHANGED`. `8`-`12` with `10` characters that must differ leaves an 8-character password with only room for two carried-over characters. That is defensible as policy and frequently unintended.

### Expiration, warning and grace are three separate parameters

| Parameter                 | Policy `4000` | Meaning                                                                                                              |
| ------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
| `PWD_EXPIRATION`          | `90`          | Maximum password age in days                                                                                         |
| `PWD_EXP_WARN`            | `2`           | Days before expiry that the warning starts. Group `MISC`                                                             |
| `PWD_EXP_GRACE`           | `1`           | Days after expiry during which sign-in still succeeds                                                                |
| `PWD_EXPIRATION_ON_RESET` | `2`           | Age applied to an administrator-issued credential                                                                    |
| `PASSWORD_NEVER_EXPIRE`   | `false`       | Group `PSWD_CHANGE_RULE`. The supported way to make a password non-expiring — do not use a 9999-day `PWD_EXPIRATION` |

All four numeric values ship set. A two-day warning window before a 90-day expiry is short; if your service desk expects a fortnight's notice, `PWD_EXP_WARN` is the field, and 90 minus grace is the effective deadline.

`NUM_DAYS_FRGT_PWD_TOKEN_VALID` is the counter-example that proves the "absent is not zero" point: it ships **empty**, and rather than treating that as zero OpenIAM records *"Can't parse the '%s' policy attribute… Defaulting…"* in the ESB log and uses **three days**. Your reset links live three days whether or not anyone chose that, and the policy screen shows you a blank field, not a 3. That log line is the only place the fallback is visible.

## A new password rule needs a code change, not configuration

`POLICY_DEF_PARAM.POLICY_PARAM_HANDLER` names the handler that enforces each parameter. Those handlers are compiled into the product, not editable expressions — you can change what a parameter is set to, and you cannot change what it means or add a parameter that does not already exist.

{% hint style="danger" %}
**You cannot express an arbitrary new password rule by editing a policy.** There is no regular-expression field for passwords anywhere in the policy model. The `RegularExpression`-typed *system properties* — the ones with a `Type` of `RegularExpression` in **Administration → System Configuration** — validate login IDs, first names and last names. They are not consulted when a password is checked. A genuinely new rule requires a new handler class, which is [Developer Guide](/administration-guide/admin.md#which-guide-owns-the-task) territory, not configuration.
{% endhint %}

The nearest configuration-level escape hatches are `REJECT_WORDS_IN_PSWD` with an uploaded dictionary, `REJECT_CHARS_IN_PSWD`, and the composition minima. Everything else is code.

## Related

* [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md) — all 59 parameters with handlers and shipped values
* [Configure password recovery and account unlock](/administration-guide/security/password-recovery.md) — the `FORGET_PSWD` group in operational context
* [Compromised password detection](/administration-guide/security/compromised-passwords.md) — `PASS_COMPROMISED_CHECK`
* [The authentication model](/administration-guide/security/authentication-model.md) — where the provider's `POLICY_ID` fits
* [Change configuration safely](/administration-guide/start/change-safely.md)

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/security/password-policy.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
