Skip to main content

PAM access policies

The rules that decide who may open a privileged session, how long for, and with whose approval — plus the fields that are stored and never enforced.

Written for whoever runs IT11 min readUpdated

A policy is the rule that decides what happens when somebody asks to use a privileged credential: whether they need approval, whether they need a second authentication factor, how long they may ask for, and how many sessions may run at once. Everything else in the privileged access module is machinery. This is where you state your intent.

This article is also, unavoidably, about defaults. Two of the three worst defects this module has had were default-value defects, and one of them was a control that reported itself as switched on while doing nothing at all. Both are fixed. Understanding what went wrong is the fastest way to understand why the policy model looks the way it does, so the last section covers it.

What a policy contains, and what is actually enforced

Be careful with this table. Four of the fields do something. Three are stored, returned by the API, offered by the console, and never read by any decision.

Every field on an access policy, its default for a newly created policy, and whether it is enforced today
FieldDefaultEnforced
require_approvaltrueYes. Without an approved, unspent, unexpired grant the session is refused with approval required: submit a JIT access request first.
require_mfafalseYes. A missing or wrong token is refused with 401 and MFA verification required for this privileged session. If the policy requires it and no verifier is available, the request fails closed with a 500 rather than proceeding.
max_concurrent_sessions1Yes. A second session on the same credential is refused with 409 and maximum concurrent sessions reached for this credential.
max_duration_min60Partly. It caps how long an access request may ask for. It does not end a running session — see below.
record_sessiontrueYes. Setting this to false stores no recording and builds no keystroke index. The session is still audited; only its content is absent. When several policies cover one credential, recording happens if any of them requires it.
allowed_time_windowempty (always)Yes. A session requested outside the window is refused. Format: Mon-Fri 09:00-18:00 UTC, or a bare 09:00-18:00 for every day; overnight windows wrap; ; separates several windows; an IANA zone name is honoured. Cron expressions are rejected when you save.
role_idunsetYes. A policy naming a role restricts the credential to members of that role in the same organisation.

All four guardrails now fail closed. That is a change worth stating plainly: allowed_time_window previously failed unsafe — it read as a restriction and imposed none — while record_session failed safe by recording regardless. Both are now honoured as written, role_id is checked, and max_duration_min ends a running session rather than only capping what a request may ask for.

A second, org-wide layer: Master Policy

Everything above is an access policy — approval, MFA, duration, concurrency, the time window. Sitting above it is a separate set of seven yes/no controls called Master Policy, on the same org-scope policy row: require a reason, require a ticket, exclusive access (an automatic checkout hold), one-time password (rotate after the session ends), live monitor, block clipboard, and block drive redirection. They govern the shape of the session and the paperwork around it, rather than who may start one, and a safe can carry an exception that loosens or tightens one control without touching the org baseline. See Master Policy and per-safe exceptions for the full reference — it is worth reading before you turn on the ticket control specifically, which has a real prerequisite.

Who can read a session recording

Session metadata — who connected to which host, when, for how long, under whose approval — is visible to anybody with pam:sessions.read, which the built-in viewer and member roles hold. Session content — the recording and the searchable transcript, meaning the keystrokes and the command output — requires a separate permission, pam:session_content.read, which no built-in role except admin holds. If you want an auditor to be able to replay recordings, you have to grant it deliberately.

Recordings are encrypted at rest with a key held separately from the recording itself, and they are not kept forever. The default retention is 365 days, configurable per organisation between 30 days and 7 years with pam:session_content.manage. The floor is a floor: retention cannot be set low enough to erase the privileged-access trail. Shortening retention applies to recordings made after the change, never retroactively. When a recording is purged, the payload and the transcript are destroyed but the record that it existed — session, host, duration, and the checksum of what was purged — is kept, so a purged recording never looks like a session that was never recorded.

Scope: which credentials a policy governs

A policy has one of three scopes, and you do not set it directly — it is inferred from what you attach the policy to, so that a policy's declared scope can never disagree with its bindings.

`credential`
Attached to one credential. The console summarises it as Applies to one credential.
`target`
Attached to one target — one machine. It governs every credential bound to that machine. Summarised as Applies to every credential on one target.
`organization`
Attached to neither, which is what you get when you write a rule for everything. Summarised as Applies to every credential in this organization. This is the scope to use for your baseline policy.

A credential can be governed by all three at once: one attached to it, one attached to its machine, and the organisation-wide baseline.

How overlapping policies combine

Every matching active policy is applied. Not the most specific one — every one. The practical rule is therefore most restrictive wins, and it has a consequence you need to know before you design around it:

A per-credential exemption is impossible. If your organisation-wide policy requires approval, a credential-scoped policy saying approval is not required does not override it; both are applied and approval is still required. There is machinery in the query that orders the results most-specific-first, and the code that evaluates them does not use that order. So the behaviour is safe and it is not what the ordering implies. If you need one credential to behave differently, that is a conversation with us rather than a setting.

One more piece of the enforcement worth knowing: cross-tenant matching is impossible by construction. The query that finds a credential's policies re-derives the organisation from the credential itself rather than trusting an identifier passed in, so a policy belonging to another customer cannot match yours even in principle.

Duration: what the cap actually caps

max_duration_min is checked when somebody raises an access request. A request over the cap is refused with both numbers in the message, which saves everyone a guessing game:

action violates privilege elevation policy: requested duration 240 min exceeds policy max 60 min

Two limitations sit on top of that, and together they mean you should not read a duration cap as a session length limit.

  • Nothing terminates a session when its grant expires. The expiry is checked when the session starts and never again. The only automatic bound on a live session is the gateway's own ceiling of eight hours. A policy saying sixty minutes refuses longer requests; it does not cut a session off at the hour. Terminating is a manual action, and it is worth making it routine.
  • A request that omits the duration bypasses the cap. The cap is applied to the value exactly as submitted, before the sixty-minute default is filled in. A request with no duration field is therefore checked against zero, passes any cap, and then becomes sixty minutes. So a cap below 60 does not constrain such a request. This is a defect, not a design.

Because a grant's clock starts when the request is raised rather than when it is approved, a tight duration cap also produces grants that expire during the approval wait. Sixty minutes is a sensible cap for that reason as well as the obvious one.

The defaults, and the pointer that makes them safe

Create a policy with nothing but a name and you get: approval required, session recorded, one hour maximum, one session at a time, no second factor, active. The console states it plainly: Unless you change them: approval is required, the session is recorded, it may last one hour, and one session may run at a time.

There is a small technical decision behind that which is worth explaining, because it is the shape of the whole problem this article is about. The three switch-like controls are represented in the API as optional values rather than plain true-or-false ones. The reason, from the code:

With a plain boolean, JSON omission and an explicit false are indistinguishable, so every policy created by a console that had not yet added the checkbox would have arrived as require_approval=false — a policy that reads as a control and permits unapproved checkout. The pointer makes 'not stated' mean 'use the safe default'.
The comment above the policy creation code

Read that twice, because it is the general case. The failure it prevents is not a bug in the security logic. The security logic was perfect. The failure is that a field nobody filled in looked exactly like a field somebody had deliberately set to permit, and the system had no way to tell the difference. An omitted field can no longer mean permit. It means: use the safe value.

The same reasoning applies to updates. Every field on an update is optional in the same way, so editing one setting on a policy cannot silently reset the others to their zero values. The console says so too: anything you do not change keeps its default.

The two historical defects, and why they are worth telling you about

Both are fixed. We describe them because they explain the current design better than any argument for it could, and because a customer who ran an earlier release should know what was and was not being enforced.

Organisation-wide policies never applied

The query that found a credential's policies filtered on the credential identifier. An organisation-wide policy has no credential identifier — that is what makes it organisation-wide — so it was never returned. The code's own account of it:

A policy with a NULL credential_id — which is what you get when an administrator writes 'everything in this organization requires approval' — was never returned, so StartSession never saw it and never applied it. The policy appeared in the list, said require_approval = true, and did nothing. That is a security control that reports itself as on while being off, which is worse than not having it.
The comment on the policy lookup

The fix matches all three scopes. A migration also re-labelled the existing rows that had been silently doing nothing, with a note that they start being enforced now, which is what their author intended when they created them. If you wrote an organisation-wide policy on an earlier release and wondered why sessions started without approval, that was this, and it is over.

The defaults were eight hours with approval off

A policy created without explicit values used to allow a 480-minute session and not require approval. Both halves are now the opposite: sixty minutes, approval on, recording on. Two things follow that are worth checking on your own tenant.

  • Open any policy you created before this change and read its values. The change fixed the default for new policies; it did not rewrite existing rows. A policy created in the old release still says what it said.
  • The database's own default is still 480 minutes. The sixty-minute default is applied by the API, not by the schema. Any row inserted by something other than the normal policy-creation path — a migration, a direct database write, a data import — gets 480. If you have a policy showing 480 that you do not remember setting, that is where it came from.

Neither of these defects was a case of the security model being wrong. In both, the model was right and the plumbing quietly disagreed with it. That is the characteristic failure mode of security software, and it is the reason this knowledge base tells you which fields are enforced rather than which fields exist.

Deleting a policy

Policies can be deleted rather than only deactivated, because an access control an administrator can create but not remove is not a manageable object. Deletion writes a hash-chained audit entry — not merely a log line — carrying the policy's name, scope and whether it required approval and recording. The reasoning is stated where the code does it:

Removing an access control is exactly the action an audit chain exists for: after this, sessions that needed approval may not.
The comment on policy deletion

The console warns you what will happen rather than asking whether you are sure: Delete {{name}}? Sessions it covers will fall back to the remaining policies. If that leaves no remaining policies, remember what the first callout in this article said — the credentials it covered now have no controls at all.

Reading policies needs the policy-read permission, which the member and viewer roles both hold: knowing what the rules are is not sensitive. Creating, editing and deleting are admin-only.

  1. Create one organisation-wide policy first

    Name it something like Baseline privileged access. Leave every setting at its default: approval required, one hour, one concurrent session. Turn on require_mfa as well — it is off by default and it is the cheapest real control you have. This single policy closes the unpoliced-credential gap.

  2. Do not use the time window

    Leave it empty. A restriction that is not enforced is worse than an absent one, because it makes you believe you have a control.

  3. Add a target-scoped policy only where the machine genuinely differs

    A domain controller or a payment system might justify a shorter duration or a higher approver count. Remember that policies combine as most-restrictive-wins, so a target policy can tighten but never loosen your baseline.

  4. Review the policy list against your actual credentials

    For each credential in the vault, ask which policies match it. Anything covered only by the baseline is fine. Anything covered by nothing means the baseline is missing or inactive.

What to do next

  1. Read the approval workflow — the control your policy turns on has two gaps you should compensate for.
  2. Read how duration and expiry behave before setting a tight cap.
  3. Read what recording actually captures, since it happens whether your policy asks for it or not.
  4. Set your Master Policy baseline — it is a separate layer from this one, and it is where the ticket, exclusive-access and live-monitor controls live.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in privileged access