Part 2 of 4 in the Salesforce Delivery Team Access series. Start with part 1, which establishes why developers rarely need Modify All Data in the first place.
TL;DR
- Design access per environment tier, not per sandbox, so that adding an environment never requires a new access design.
- Full-copy sandboxes hold real records, which means they deserve production rules. This is the tier teams get wrong.
- Every delivery team user is Minimum Access - Salesforce plus a role permission set group. If the profile grants a capability, the model has already leaked.
- The mechanism:
PermissionSetAssignmentis an SObject, not a metadata type. A developer can build a permission set granting Modify All Data, deploy it to production, and it grants nothing, because assigning it needs permissions no delivery role holds. - The weakness: profiles are metadata, so a profile edited in a sandbox can propagate. Keep the baseline at Minimum Access and diff
Profileon every pull request. - Standing production access for a developer is Setup read plus metadata retrieve. Everything else, including debug logs, is step-up with an expiry.
What You'll Learn
- A five-tier environment model that scales without redesign as sandboxes come and go
- How to layer the baseline profile, capability permission sets, role groups, and session-based step-up
- Exactly where the privilege escalation chain breaks, and the one place it genuinely leaks
- Standing access by role for developers, testers, functional consultants, DevOps engineers, and support
- Why sandbox refresh becomes a hard dependency under this model, and what that costs you
The Problem
Part 1 dealt with the permission. This part deals with the structure, and the structural question is the one people actually ask: how do you stop a developer holding System Administrator in every sandbox and in production?
The naive answer is to write a policy. Policies do not survive a release weekend. What survives is a design where the escalation simply is not available, and where the daily work of the team is unaffected enough that nobody needs to route around it.
That requires two things most access designs skip. First, a way to talk about environments that does not break every time someone spins up a sandbox. Second, an understanding of which controls are technical boundaries and which are merely conventions, because teams routinely rely on the second while believing they have the first.
Common Questions This Article Answers:
- How do I stop developers holding System Administrator across every sandbox and production?
- If a developer can deploy a permission set, can they escalate their own access?
- What should a developer actually be able to do in production?
Quick Answer
Stop designing access per sandbox and design it per tier: developer, integration, acceptance, staging, production. Treat any full-copy sandbox holding real records as production, because it is. Put every delivery team member on Minimum Access - Salesforce and deliver all capability through permission set groups named by role and tier, so the same metadata exists in every org while only the assignments differ.
The reason that holds is that assignment is not metadata. PermissionSetAssignment is an SObject, so it does not travel in a package or a change set. A developer can create a permission set granting Modify All Data in a sandbox, commit it, and deploy it to production, where it will grant absolutely nothing, because assigning it requires Manage Users and Assign Permission Sets and no delivery role holds either. They can build the key and ship it. They cannot put it in anyone's hand.
The exception, and it is the one to watch, is that profiles are metadata and do propagate. Keep the baseline profile at Minimum Access and review profile diffs on every pull request.
Tier the environments, not the sandboxes
| Tier | Examples | Data | Default posture |
|---|---|---|---|
| T0 Developer | Scratch orgs, dev sandboxes | Synthetic only | Broad build access is fine. Elevation is the default. |
| T1 Integration | SIT, the CI target | Synthetic and masked | Humans read, the pipeline writes. |
| T2 Acceptance | UAT, training | Masked copy | Config and test access. No Apex authoring, no data export. |
| T3 Staging | Full copy, Partial Copy, pre-prod | Real records | Treated as production. |
| T4 Production | Production | Real | Standing access is read and diagnose only. |
T3 is the tier teams get wrong, and the classifier is the data, not the sandbox type. Full Copy and Partial Copy both contain real production records, so both land in T3 by default. Partial Copy surprises people because they think of it as small, but a sample of real patient or customer data is still real data. A sandbox drops to T2 only when masking has been proven, which in practice means Salesforce Data Mask configured and verified, not assumed. Absent that, treat it as T3 and give it production rules. Teams that would never grant a contractor production access routinely grant them full-copy or partial-copy sandbox access without noticing they are the same exposure.
This has a direct consequence for the adoption order in part 3, and it is worth stating here so the phasing does not read as painless. If most of your sandboxes hold real data, then "restrict production first and leave the sandboxes alone" is a deliberate decision to leave real data broadly accessible for a while, not a free lunch. It is still the right place to start, because production is the highest-value target and the fastest win, but the size of the follow-on work scales with how many real-data sandboxes you have, which for most orgs is most of them.
The layer model
Every delivery team user is assembled the same way.
Profile: Minimum Access - Salesforce. One profile for the whole delivery team, or a clone of it carrying login IP ranges, login hours, password policy, and session timeout. No administrative permissions at all. If the profile grants a capability, the model has already leaked.
Salesforce's own position here shifted in an instructive way. The retirement of permissions on profiles, originally targeted at Spring '26, was cancelled in 2026 after customer feedback and remaining feature gaps. The deadline died. The recommendation did not: profiles for baseline settings, permission sets for access. You can adopt the practice without needing the mandate.
Capability permission sets: CAP_<capability>. Small and single-purpose so they compose. CAP_Setup_Read for View Setup and Configuration, CAP_Apex_Author, CAP_Metadata_Deploy, CAP_User_Reset. Two rules keep them clean: no capability set mixes a build permission with a data permission, and object and field access lives in separate functional permission sets owned by the application team. Whether a developer can see patient data is a business access decision, not a delivery access decision, and conflating the two is how permission sets become impossible to review.
That said, do not decompose prematurely. At small scale a flat permission set per role is easier to review than ten capability sets composed into groups, and part 3 covers when the decomposition earns its keep.
Role permission set groups: ROLE_<role>_<tier>. The job, in this tier. The same metadata exists in every org, and only the assignment differs per environment.
Session-based groups: STEPUP_<capability>. Activated by an approval flow running in system context, expiring at session end. The requester never holds the permission that activates it. This is the right mechanism for the "developer needs to look at production for an hour" case, which is the overwhelming majority of production access requests.
Where the escalation chain breaks
Here is the part that makes the whole thing hold, and it is worth understanding precisely because it is counterintuitive.
A developer in a T0 sandbox has real power. They can create a permission set granting Modify All Data. Permission sets are metadata, so they can commit it, merge it, and deploy it to production. So why is production safe?
Because assignment is not metadata.
PermissionSetAssignment is an SObject. It is data. There is no Metadata API type for it, it does not travel in a package, and it does not travel in a change set. The developer can deploy PS_Give_Me_Everything into production and it will sit there granting absolutely nothing, because assigning it requires Manage Users and Assign Permission Sets, which no delivery role holds in T4.
The developer can build the key and even ship it to production. They cannot put it in anybody's hand.
- STEP 01Build in sandboxDeveloper creates a permission set granting Modify All Data. Entirely permitted in a dev sandbox.
- STEP 02Commit and mergePermission sets are metadata. They travel in the package like any other component.
- STEP 03Deploy to productionThe permission set now exists in production. It grants nothing to anybody.
- STEP 04Assign itPermissionSetAssignment is an SObject. It is data, not metadata: no Metadata API type, no package, no change set. Assigning needs Manage Users and Assign Permission Sets, which no delivery role holds.
- STEP 05Effective accessNone. The key exists. It is in nobody’s hand.
That gives three enforcement points, in descending order of reliability:
- Assignment control. Only the platform team assigns in T3 and T4. Assigning a permission set requires Manage Users or Assign Permission Sets, and no delivery role holds either. Withhold those two permissions and a human cannot self-assign in Setup.
- Profile immutability. Profiles are metadata, so a profile edit in a sandbox can propagate. This is the leak in the model, and it deserves to be named as such. It is exactly why the baseline profile stays at Minimum Access and why
Profileshould be excluded from the deployment manifest or diffed on every pull request. - Pipeline policy check. Fail the build when a deploy introduces a permission set containing a high-risk permission. Cheap, and bypassable by anyone who can edit the pipeline, which is a real problem covered in part 4.
Two honest caveats, because the first point is doing the heavy lifting and it is not as absolute as it first looks.
The first is assignment by automation rather than by hand. Apex can insert PermissionSetAssignment records, and it is worth being precise about why that is not a free escape hatch: assigning a permission set is gated by an administrative permission, and Apex system mode does not grant administrative permissions the way it waives object and field permissions. Code that runs as a locked-down identity is blocked from assigning exactly as a locked-down human is. The exposure appears only when the code runs as an identity that already holds Manage Users or Assign Permission Sets, which is why a post-install script or a scheduled job executed by a privileged automation user is the real vector, not a developer's committed Apex on its own. PermissionSetAssignment is also created automatically by User Access Policies, which part 3 recommends for provisioning: anyone who can edit a policy grants access at scale without ever touching an individual assignment. So the never-assign list is not two permissions, it is Manage Users, Assign Permission Sets, and the permission governing User Access Policies, plus control over who can edit the pipeline and the policies themselves. The boundary holds against people and against locked-down code; it is only as strong as the privilege of the automation allowed to cross it, which routes straight back to pipeline control.
The second is that only the first point is even arguably a technical boundary. The second is a review discipline and the third is a convention. Teams commonly believe they have three controls when they have one and a half, and it is worth being clear internally about which is which.
Standing access by role
Standing access is the minimum that lets someone work unattended. Anything above it is step-up. The T4 column is the point of the whole exercise.
Developer. In production: View Setup and Configuration, API Enabled, and debug log access with approval. That is the complete list, and it covers most "I need prod access to debug this" requests. Worth knowing: sf project retrieve start against production works with View Setup and Configuration plus API Enabled. Developers routinely assume production retrieve requires administrator rights. It does not.
The debug log line deserves the warning from part 1 repeated here, because it is where this model is most likely to be quietly widened. Debug log access in Setup requires View All Data, and there is no narrower permission. So it is org-wide read on every record of every object. Treat it as step-up with an expiry, never as standing access, and size the approval accordingly.
Tester. The important decision is not what the tester user can do, it is that testers should not be testing as themselves. A tester logged in with a tester permission set is exercising a permission profile no real user has, which means access defects stay invisible until UAT. Use persona users carrying the real business profile in T0 through T2. Log In As is cleaner but requires Manage Users, which is a privilege-escalation permission, so persona credentials win.
Functional consultant. Customize Application and Manage Flow in T0 and T2, not in T3 or T4. T2 keeps it because UAT turnaround is a genuine delivery constraint and the data is masked. T3 does not, because the data is real. Managing reports and dashboards stays standing in production deliberately: low blast radius, high support cost if withheld.
DevOps engineer (human). Their job is to operate the pipeline, not to be the pipeline. Standing production access is read plus sandbox management. Deploy capability in T1 and above is step-up, because a human deploying directly is a pipeline bypass and should feel like one. Manage Certificates is step-up only in production, because it is the permission that lets someone rotate the JWT signing certificate, which is the permission that lets someone re-point the CI identity.
Support and L2. The obvious design gives this role standing debug log access in production, so that L2 can answer a ticket without escalating to a developer. Because debug logs require View All Data, that obvious design is wrong: it hands standing org-wide data read to your largest and most frequently changing team. Make it step-up like everyone else and accept the small escalation cost, or narrow the role until the elevation is genuinely rare. Password resets should use delegated administration rather than Manage Users, which is the cleanest privilege reduction available in the whole model and is badly under-used. Data correction in production is step-up with a ticket reference, granting edit on a named object list, never Modify All Data.
Sandbox refresh becomes a dependency
A sandbox is a copy of production, so it inherits production's assignments, which under this model are deliberately minimal. A freshly refreshed sandbox therefore contains a delivery team that cannot work. That is correct behaviour, and it must be automated rather than manually repaired.
Implement SandboxPostCopy to run on every create and refresh: assign the tier-appropriate role permission set groups from a roster held in custom metadata rather than a hardcoded username list, scramble email addresses on copied users, deactivate every production user not on the delivery roster, and clear production named credentials.
One constraint to design around: SandboxPostCopy subclasses execute in a single transaction and are subject to per-transaction Apex limits. For a large roster or a large copied user population, enqueue the work rather than doing it inline.
Be clear-eyed about what this introduces. Today a refresh is harmless, because everyone is still an administrator and work continues. Under this model, a failed post-copy blocks the entire team, and it fails silently until somebody tries to work. That Apex needs test coverage and a documented manual fallback before it goes anywhere near a shared sandbox.
This is also the strongest argument for not restricting sandboxes at all until you have to. Part 3 makes that case properly.
Frequently Asked Questions
Q: If a developer can deploy a permission set to production, can they escalate their own access?
A: No, and this holds for code as well as for people, which is the part that surprises developers. PermissionSetAssignment is an SObject rather than a metadata type, so it does not travel in a package or change set. Assigning a permission set requires Manage Users or Assign Permission Sets, and Apex system mode does not grant those the way it waives object and field permissions, so a developer's committed Apex running as a locked-down identity is blocked from assigning just as a locked-down human is. The exposure is narrower and more specific: automation running as an identity that already holds those permissions, such as a post-install script or scheduled job under a privileged user, or a User Access Policy that anyone with the policy permission can edit. That is why the assignment boundary ultimately depends on who controls the pipeline and who can edit access policies, and why those belong on the same never-grant list. The other genuine risk is profiles, which are metadata and do propagate, so keep the baseline profile at Minimum Access and review profile diffs on every pull request.
Q: Why treat a full-copy sandbox as production?
A: Because it holds production records. The access question is about what data a person can reach, not what the environment is called. If your full copy is genuinely masked and you can demonstrate that, treat it as acceptance tier. If masking is assumed rather than proven, it is staging and it inherits production rules. Most teams that would never grant a contractor production access have granted the same person full-copy sandbox access without noticing the equivalence.
Q: Won't this slow the team down during build?
A: It should not, if you scope it correctly, because the friction lands where the work does not. Developer sandboxes keep broad access. What changes is production and full-copy sandboxes, where developers rarely work day to day. The model does fail if your deployment pipeline is slow, because removing production configuration access means routine changes become deployments. Measure your median deploy time first. If it is over roughly fifteen minutes, the pipeline is the problem to solve before the permissions are.
Q: Do we have to build SandboxPostCopy?
A: Only if you restrict sandbox access, and you probably should not start there. Restricting production and full-copy sandboxes captures most of the risk reduction and requires no post-copy automation at all, because you are not changing what happens in developer or integration sandboxes. Post-copy Apex has an unforgiving operational profile: it runs rarely, so it is under-tested, and when it fails it blocks everyone. Defer it.
Key Takeaways
- Tier the environments, not the sandboxes: adding a sandbox should never require a new access design.
- Full-copy sandboxes are production: they hold real records, so they inherit production rules.
- Profile is identity, permission set is capability: one baseline profile, all power through groups.
- Assignment is data, not metadata: that single fact is what stops sandbox elevation reaching production.
- Profiles are the leak: they are metadata and they do propagate, so diff them on every pull request.
- Only one of your three controls is a real boundary: assignment control. The other two are discipline and convention.
- Debug logs are step-up, never standing: they require View All Data regardless of how routine the task is.
What's Next?
Recommended Reading:
- Part 3: Sizing the model to your team for team shapes, internal versus external admins, and what this costs to run
- Part 4: Deployable permission sets for the actual metadata, the CI user, and OmniStudio
- Locked Out After Salesforce MFA Enforcement for designing the break-glass accounts this model depends on
- Team Topologies for Salesforce: The Org Is the Fracture Plane for why the tier boundary is really an org-boundary question
- Free Salesforce Event Monitoring: Build a Security Baseline from EventLogFile for the detection layer underneath all of this
Action Items:
- Classify every sandbox you have into the five tiers, and be honest about which full copies are actually masked.
- Reclassify every unmasked full-copy sandbox as production for access purposes, then list who loses access. That list is your real exposure today.
- Check whether
Profileis in your deployment manifest. If it is, decide whether to exclude it or to diff it on every pull request. - Write down which of your three enforcement points are technical boundaries and which are conventions.
Resources & References
- Permissions in Profiles Retirement Cancelled - Salesforce Help
- Session-Based Permission Set Groups - Salesforce Help
- SandboxPostCopy Interface - Apex Reference Guide
- User Access Policy Considerations - Salesforce Help
About This Guide: Part 2 of the Salesforce Delivery Team Access series. Last updated July 2026. Verify permission behaviour in an integration sandbox before adopting it, since it changes between releases.
Tags: #salesforce #security #leastprivilege #devops #salesforceadmin