API-Only User Hardening
An integration identity is the highest-value credential in most orgs. It is
long-lived, it usually carries broader object access than any human, and its secret
sits in a config store outside Salesforce's control. Hardening it is four decisions
plus a monitoring commitment:
| Decision |
Hardened choice |
| Licence |
Salesforce Integration user license |
| Profile |
Minimum Access - API Only Integrations — not a clone of anything |
| Scope |
One permission set, named objects, named operations, viewAllRecords over ViewAllData |
| Authentication |
OAuth 2.0 client credentials or JWT bearer — no password anywhere |
| Ongoing |
Named owner, exported login history, alerting, rotation cadence |
The permission that makes the rest coherent:
"If a user has the API Only User permission, they can access Salesforce only via
APIs, regardless of their other permissions."
— Metadata API Developer Guide
"Regardless of their other permissions" makes this a mode switch, not a permission
among many.
Before Starting
Count the integrations against the org's licences. "One or more Salesforce
Integration user licenses are available by default in Enterprise, Unlimited,
Performance, and Developer editions, with more add-on licenses available to
purchase." A one-identity-per-integration design is a procurement item first.
Enumerate the objects and operations this integration actually performs. Not
"needs access to Salesforce" — the object list and, per object, whether it reads,
creates, edits, or deletes.
Ask whether the caller has provisioned static egress, in both IPv4 and IPv6.
If not, IP restriction is unavailable and the posture must be carried elsewhere.
Check whether anything renders UI as this identity before enabling API Only
User on an existing account — Visualforce, Lightning Out, Experience Cloud, a
scheduled report. If something does, that is a second integration sharing the
account.
Core Concepts
The licence carries a contractual boundary
"The Salesforce Integration API permission set license extends and restricts
specific user and object permissions for system-to-system integrations. It may not
be used for human users to access Salesforce data or features through any user
interface."
A human using an integration licence is a licence-compliance problem as well as a
security one. That boundary is a feature — it makes accidental reuse an event rather
than a convenience.
The client credentials flow enforces API-only
<oauthConfig>
<isClientCredentialEnabled>true</isClientCredentialEnabled>
<oauthClientCredentialUser>etl.warehouse@example.com</oauthClientCredentialUser>
</oauthConfig>
"oauthClientCredentialUser — The execution user for the OAuth 2.0 client
credentials flow. Salesforce returns access tokens on behalf of this user. This
user must have the API Only permission."
The platform will not let you bind a machine flow to a UI-capable identity. Both
fields are API 56.0 and later.
Scope with viewAllRecords, not ViewAllData
<objectPermissions>
<object>Opportunity</object>
<allowRead>true</allowRead>
<viewAllRecords>true</viewAllRecords>
<allowCreate>false</allowCreate>
<allowEdit>false</allowEdit>
<allowDelete>false</allowDelete>
<modifyAllRecords>false</modifyAllRecords>
</objectPermissions>
Per-object, review-visible, and defensible line by line. Modify All Data grants
read, create, edit, and delete on every object and bypasses all sharing.
The IP control is split across two screens
Profile loginIpRanges sets the restriction; the connected app's ipRelaxation
decides whether that app honours it. ENFORCE (default) honours it; BYPASS
"Allows a user to run this app without org IP restrictions." Audit both together or
you are auditing neither.
Passwords bring three inherited problems
Expiry from a cloned profile (ProfileSessionSetting.passwordExpiration — 0, 30,
60, 90, 180, 365, where 0 never expires), the MFA requirement that applies
to direct logins, and a copyable credential in a config store. Salesforce's own
guidance: "For security, we recommend blocking user-agent and username-password
flows."
Login History is not your system of record
"The Login History page shows up to 20,000 records of user logins for the past 6
months."
For a frequent integration that is days. Export on a schedule or stream to a SIEM,
and decide this at provisioning — unretained data cannot be recovered.
Common Patterns
Pattern A — the hardened baseline
Salesforce Integration licence → Minimum Access - API Only Integrations → one
scoped permission set → connected app with client credentials, ipRelaxation set to
ENFORCE, and profile Login IP Ranges. Full metadata in
references/examples.md, Example 1.
Pattern B — JWT bearer where no secret may cross the wire
The client signs an assertion with a private key; Salesforce validates against an
uploaded certificate. Rotation becomes a certificate swap. Choose this where the
client already has key management, or where the shared secret would otherwise live
in a shared config store.
Pattern C — parallel-run migration off password auth
Stand up the hardened identity alongside the legacy one, cut traffic over behind a
flag, and only then remove the old credential. The completion criterion is thirty
days with no username-password subtype in Login History — not "the new flow works."
Pattern D — the integration section of the access review
Integration identities have no manager and no role, so they fall out of
manager-based reviews entirely. Give them their own section with their own
questions: still live, permission set still minimal, IP range current, secret last
rotated, licence still needed.
Decision Guidance
| Situation |
Approach |
| New server-to-server integration |
Salesforce Integration licence, Minimum Access - API Only Integrations, client credentials flow |
| Client already has key management, or secret would be shared |
JWT bearer flow |
| Caller has provisioned static egress |
Profile Login IP Ranges + ipRelaxation ENFORCE |
| Caller cannot provide static egress |
No IP control — say so; carry posture with scope, short-lived tokens, and monitoring |
| Job must see all records on two objects |
viewAllRecords on those two objects only |
| Job writes to one object |
modifyAllRecords on that object only |
Existing shared integration@ account |
Split per integration; prove the sharing with Login History first |
| Legacy password integration that cannot change this release |
passwordExpiration 0 as a commented, owned, dated debt marker |
| Identity also renders a Visualforce page |
Two integrations sharing one account — split before enabling API Only User |
Recommended Workflow
- Scope the integration: name the objects and, per object, the operations. Get
the caller's egress addresses in both address families, with an owner and a
renewal date.
- Provision the identity: Salesforce Integration licence, the
Minimum Access - API Only Integrations profile, one permission set granting
exactly the scope from step 1 using per-object viewAllRecords /
modifyAllRecords rather than org-wide permissions.
- Configure authentication with no password: connected app with
isClientCredentialEnabled and oauthClientCredentialUser (or JWT bearer),
isPkceRequired where the flow supports it, and ipRelaxation set explicitly to
ENFORCE.
- Apply Login IP Ranges to the integration profile, one range per address per
family, each with a
description naming the owner and renewal date.
- Wire monitoring before go-live: scheduled Login History export or SIEM
stream, plus alerts on off-hours logins, unexpected source IPs, query-volume
changes, and any username-password subtype.
- Record ownership: a named human owner, a secret rotation cadence, and the
identity's entry in the access review — somewhere queryable, not in a wiki.
- If migrating: parallel-run, cut over, observe for thirty days with no legacy
flow subtype in Login History, then remove the old credential and block the
legacy flows org-wide.
Review Checklist
Salesforce-Specific Gotchas
Full detail with quotes in references/gotchas.md.
- API Only User overrides every other permission and replaces the login page.
- The client credentials flow refuses a user who can log in — the execution
user must hold the API Only permission.
- A cloned profile inherits a password expiry nobody chose, and the job fails
on that day with no human to warn.
- A connected app can cancel the profile's IP restriction via
ipRelaxation.
- The Salesforce Integration licence is not a free upgrade — count before
committing to one identity per integration.
- One shared identity destroys attribution and multiplies rotation cost.
- Login History holds far less than you think — 20,000 records, six months.
- Legacy auth flows keep working after you "migrate."
Modify All Data is almost never the permission you needed.
- The integration user is invisible in most access reviews.
Output Artifacts
| Artifact |
Description |
| Integration identity record |
Licence, profile, permission set, connected app, and the named human owner, all cross-referenced |
| Scope justification |
Per object: which operations, why, and which part of the integration performs them |
| Authentication design |
Flow chosen, why, where the secret lives, and the rotation cadence |
| Network control |
IP ranges with owner and renewal date, plus the ipRelaxation value and its approver if not ENFORCE |
| Monitoring plan |
Login History export or SIEM destination, alert conditions, and who receives them |
| Migration completion evidence |
Thirty days of Login History showing no legacy flow subtype, before the old credential is removed |
Related Skills
security/ip-relaxation-and-restriction — the profile-vs-network-vs-connected-app
IP model this skill applies to one identity
security/oauth-token-management — token lifetime, refresh policy, and revocation
for the credential this identity uses
security/mfa-enforcement-patterns — why a password-based integration is a
problem the MFA programme will surface
security/event-monitoring — the data source that makes an integration identity's
behaviour observable at all
1---2name: api-only-user-hardening3description: Provision and harden integration (API-only) users: no UI login, IP restrictions, minimum permission set, session lifetime, and monitoring. NOT for human admin account hardening — use admin/integration-user-management.4---56# API-Only User Hardening78An integration identity is the highest-value credential in most orgs. It is9long-lived, it usually carries broader object access than any human, and its secret10sits in a config store outside Salesforce's control. Hardening it is four decisions11plus a monitoring commitment:1213| Decision | Hardened choice |14|---|---|15| Licence | Salesforce Integration user license |16| Profile | `Minimum Access - API Only Integrations` — not a clone of anything |17| Scope | One permission set, named objects, named operations, `viewAllRecords` over `ViewAllData` |18| Authentication | OAuth 2.0 client credentials or JWT bearer — no password anywhere |19| Ongoing | Named owner, exported login history, alerting, rotation cadence |2021The permission that makes the rest coherent:2223> "If a user has the API Only User permission, they can access Salesforce only via24> APIs, regardless of their other permissions."25> — Metadata API Developer Guide2627"Regardless of their other permissions" makes this a mode switch, not a permission28among many.2930---3132## Before Starting33341. **Count the integrations against the org's licences.** "One or more Salesforce35 Integration user licenses are available by default in Enterprise, Unlimited,36 Performance, and Developer editions, with more add-on licenses available to37 purchase." A one-identity-per-integration design is a procurement item first.38392. **Enumerate the objects and operations this integration actually performs.** Not40 "needs access to Salesforce" — the object list and, per object, whether it reads,41 creates, edits, or deletes.42433. **Ask whether the caller has provisioned static egress**, in both IPv4 and IPv6.44 If not, IP restriction is unavailable and the posture must be carried elsewhere.45464. **Check whether anything renders UI as this identity** before enabling API Only47 User on an existing account — Visualforce, Lightning Out, Experience Cloud, a48 scheduled report. If something does, that is a second integration sharing the49 account.5051---5253## Core Concepts5455### The licence carries a contractual boundary5657> "The Salesforce Integration API permission set license extends and restricts58> specific user and object permissions for system-to-system integrations. It may not59> be used for human users to access Salesforce data or features through any user60> interface."6162A human using an integration licence is a licence-compliance problem as well as a63security one. That boundary is a feature — it makes accidental reuse an event rather64than a convenience.6566### The client credentials flow enforces API-only6768```xml69<oauthConfig>70 <isClientCredentialEnabled>true</isClientCredentialEnabled>71 <oauthClientCredentialUser>etl.warehouse@example.com</oauthClientCredentialUser>72</oauthConfig>73```7475> "`oauthClientCredentialUser` — The execution user for the OAuth 2.0 client76> credentials flow. Salesforce returns access tokens on behalf of this user. **This77> user must have the API Only permission.**"7879The platform will not let you bind a machine flow to a UI-capable identity. Both80fields are API 56.0 and later.8182### Scope with `viewAllRecords`, not `ViewAllData`8384```xml85<objectPermissions>86 <object>Opportunity</object>87 <allowRead>true</allowRead>88 <viewAllRecords>true</viewAllRecords>89 <allowCreate>false</allowCreate>90 <allowEdit>false</allowEdit>91 <allowDelete>false</allowDelete>92 <modifyAllRecords>false</modifyAllRecords>93</objectPermissions>94```9596Per-object, review-visible, and defensible line by line. **Modify All Data** grants97read, create, edit, and delete on every object and bypasses all sharing.9899### The IP control is split across two screens100101Profile `loginIpRanges` sets the restriction; the connected app's `ipRelaxation`102decides whether that app honours it. `ENFORCE` (default) honours it; `BYPASS`103"Allows a user to run this app without org IP restrictions." Audit both together or104you are auditing neither.105106### Passwords bring three inherited problems107108Expiry from a cloned profile (`ProfileSessionSetting.passwordExpiration` — `0`, `30`,109`60`, `90`, `180`, `365`, where `0` never expires), the MFA requirement that applies110to direct logins, and a copyable credential in a config store. Salesforce's own111guidance: "For security, we recommend blocking user-agent and username-password112flows."113114### Login History is not your system of record115116> "The Login History page shows up to 20,000 records of user logins for the past 6117> months."118119For a frequent integration that is days. Export on a schedule or stream to a SIEM,120and decide this at provisioning — unretained data cannot be recovered.121122---123124## Common Patterns125126### Pattern A — the hardened baseline127128Salesforce Integration licence → `Minimum Access - API Only Integrations` → one129scoped permission set → connected app with client credentials, `ipRelaxation` set to130`ENFORCE`, and profile Login IP Ranges. Full metadata in131[`references/examples.md`](references/examples.md), Example 1.132133### Pattern B — JWT bearer where no secret may cross the wire134135The client signs an assertion with a private key; Salesforce validates against an136uploaded certificate. Rotation becomes a certificate swap. Choose this where the137client already has key management, or where the shared secret would otherwise live138in a shared config store.139140### Pattern C — parallel-run migration off password auth141142Stand up the hardened identity alongside the legacy one, cut traffic over behind a143flag, and *only then* remove the old credential. The completion criterion is thirty144days with no username-password subtype in Login History — not "the new flow works."145146### Pattern D — the integration section of the access review147148Integration identities have no manager and no role, so they fall out of149manager-based reviews entirely. Give them their own section with their own150questions: still live, permission set still minimal, IP range current, secret last151rotated, licence still needed.152153---154155## Decision Guidance156157| Situation | Approach |158|---|---|159| New server-to-server integration | Salesforce Integration licence, `Minimum Access - API Only Integrations`, client credentials flow |160| Client already has key management, or secret would be shared | JWT bearer flow |161| Caller has provisioned static egress | Profile Login IP Ranges + `ipRelaxation` `ENFORCE` |162| Caller cannot provide static egress | No IP control — say so; carry posture with scope, short-lived tokens, and monitoring |163| Job must see all records on two objects | `viewAllRecords` on those two objects only |164| Job writes to one object | `modifyAllRecords` on that object only |165| Existing shared `integration@` account | Split per integration; prove the sharing with Login History first |166| Legacy password integration that cannot change this release | `passwordExpiration` `0` as a commented, owned, dated debt marker |167| Identity also renders a Visualforce page | Two integrations sharing one account — split before enabling API Only User |168169---170171## Recommended Workflow1721731. **Scope the integration**: name the objects and, per object, the operations. Get174 the caller's egress addresses in both address families, with an owner and a175 renewal date.1762. **Provision the identity**: Salesforce Integration licence, the177 `Minimum Access - API Only Integrations` profile, one permission set granting178 exactly the scope from step 1 using per-object `viewAllRecords` /179 `modifyAllRecords` rather than org-wide permissions.1803. **Configure authentication with no password**: connected app with181 `isClientCredentialEnabled` and `oauthClientCredentialUser` (or JWT bearer),182 `isPkceRequired` where the flow supports it, and `ipRelaxation` set explicitly to183 `ENFORCE`.1844. **Apply Login IP Ranges to the integration profile**, one range per address per185 family, each with a `description` naming the owner and renewal date.1865. **Wire monitoring before go-live**: scheduled Login History export or SIEM187 stream, plus alerts on off-hours logins, unexpected source IPs, query-volume188 changes, and any username-password subtype.1896. **Record ownership**: a named human owner, a secret rotation cadence, and the190 identity's entry in the access review — somewhere queryable, not in a wiki.1917. **If migrating**: parallel-run, cut over, observe for thirty days with no legacy192 flow subtype in Login History, then remove the old credential and block the193 legacy flows org-wide.194195---196197## Review Checklist198199- [ ] Uses the Salesforce Integration licence, not a full user licence200- [ ] Profile is `Minimum Access - API Only Integrations`, not a clone201- [ ] Permission set grants no `ViewAllData` and no `ModifyAllData`202- [ ] Every object permission is justified by a named operation the job performs203- [ ] No password in the authentication path204- [ ] `oauthClientCredentialUser` (if used) has the API Only permission205- [ ] `ipRelaxation` is explicitly `ENFORCE`, or the exception has a named approver206- [ ] Login IP Ranges cover both address families, with owner and renewal date207- [ ] One identity, one profile, one connected app, one owner — not shared208- [ ] Login History export or SIEM stream configured before go-live209- [ ] Alerting on off-hours logins, unexpected IPs, and legacy flow subtypes210- [ ] Secret rotation cadence recorded with the owner211- [ ] Identity appears in a dedicated section of the access review212- [ ] `apiOnlyUserHomePageURL` set so a confused admin lands somewhere explanatory213214---215216## Salesforce-Specific Gotchas217218Full detail with quotes in [`references/gotchas.md`](references/gotchas.md).2192201. **API Only User overrides every other permission** and replaces the login page.2212. **The client credentials flow refuses a user who can log in** — the execution222 user must hold the API Only permission.2233. **A cloned profile inherits a password expiry nobody chose**, and the job fails224 on that day with no human to warn.2254. **A connected app can cancel the profile's IP restriction** via `ipRelaxation`.2265. **The Salesforce Integration licence is not a free upgrade** — count before227 committing to one identity per integration.2286. **One shared identity destroys attribution and multiplies rotation cost.**2297. **Login History holds far less than you think** — 20,000 records, six months.2308. **Legacy auth flows keep working after you "migrate."**2319. **`Modify All Data` is almost never the permission you needed.**23210. **The integration user is invisible in most access reviews.**233234---235236## Output Artifacts237238| Artifact | Description |239|---|---|240| Integration identity record | Licence, profile, permission set, connected app, and the named human owner, all cross-referenced |241| Scope justification | Per object: which operations, why, and which part of the integration performs them |242| Authentication design | Flow chosen, why, where the secret lives, and the rotation cadence |243| Network control | IP ranges with owner and renewal date, plus the `ipRelaxation` value and its approver if not `ENFORCE` |244| Monitoring plan | Login History export or SIEM destination, alert conditions, and who receives them |245| Migration completion evidence | Thirty days of Login History showing no legacy flow subtype, before the old credential is removed |246247---248249## Related Skills250251- `security/ip-relaxation-and-restriction` — the profile-vs-network-vs-connected-app252 IP model this skill applies to one identity253- `security/oauth-token-management` — token lifetime, refresh policy, and revocation254 for the credential this identity uses255- `security/mfa-enforcement-patterns` — why a password-based integration is a256 problem the MFA programme will surface257- `security/event-monitoring` — the data source that makes an integration identity's258 behaviour observable at all