‘Temporary’ local admin usually isn’t. Here’s how we made it actually expire.
Here’s a ritual every helpdesk knows. A user needs to install something, or update a driver, or run a tool that insists on elevation, and they don’t have local admin. So you add them to the local Administrators group “just for a few minutes,” they do the thing, and you remove them again. Or you mean to. The removal is a manual step at the end of a task that’s already over in the user’s mind, so it’s the step that gets skipped, and three months later a fleet audit finds forty people with standing local admin nobody can explain.
I did this for years on a hospital fleet of roughly 30,000 endpoints. The group-membership dance is the default because it’s the only lever the OS hands you. It also has two problems that are worse than “you forget to undo it,” and both of them are about when admin actually starts and stops.
Removing someone from the group does not revoke their admin
Windows builds your access token at logon. Your group memberships are baked into that token then, and they don’t change while you’re signed in. So if you add someone to Administrators and they’re already logged in, they get nothing until they log off and back on. And the reverse is the part that bites: if you remove them from the group while they’re logged in, their token keeps the Administrators membership until they log off. For the rest of that session, they are still an admin. You changed the SAM; you did not change the token that’s actually running.
The second problem is escape. Say you’re disciplined and you script the whole thing with a timer. The user launches an elevated command prompt during their window, and from it starts a few more processes. Your timer fires, you pull them from the group, but every process that already got an admin token keeps it. The window closed; the things that walked through it are still standing.
So “temporary local admin” as everyone actually implements it is neither of the things in the name. It isn’t reliably temporary, and half the time it isn’t even scoped to the moment you granted it.
Don’t touch the group. Mint the token.
The thing we actually wanted: hand a specific program an elevated token, scoped to one launch, that exists only as long as we keep holding it - and never write to the Administrators group at all. No membership to forget, no SAM change to replicate, nothing that outlives a single click.
Windows will let you do exactly this, through a corner of the logon API called S4U (Service-for-User). The RFF agent runs as SYSTEM, which holds SeTcbPrivilege - the “act as part of the operating system” right. That privilege lets it call LsaLogonUser and mint a fresh logon token for another account without their password. You hand it the account, it hands you back a token as that user.
The trick is what you inject on the way. LsaLogonUser takes a list of extra groups to add to the token it creates (pLocalGroups). So we mint the S4U token and add the BUILTIN\Administrators SID right there, at token-creation time. The result is a token that is the user, but elevated - and it’s elevated because we built it that way this second, not because the user’s account is privileged. Close the token, and the elevation is gone. There is nothing to revoke in the SAM because we never wrote to the SAM.
One gotcha worth stating because it cost us an afternoon: a token minted this way is a full elevated token, not a UAC split token. If you check ElevationType expecting 2 (the “limited, with a linked full token” value you get from an ordinary interactive admin login), you get 1 and conclude it didn’t work. It did. The correct test is “is the Administrators SID present and is the token actually elevated,” not “which UAC flavor is this.”
That’s the idea in a paragraph. Getting it to run a program a human can see and use took a bit more time, and they’re the interesting part.
Bug one: the elevated window is black
First launch through the new token: the process starts, and its window is a black rectangle with no controls. It’s running. You just can’t see or use it.
Interactive Windows apps draw on a window station and desktop (winsta0\default), and access to those is controlled by a DACL (Discretionary Access Control List - the per-object allow/deny list Windows checks before it lets you touch something). That DACL is written to allow the logon SID of whoever is signed in interactively - a per-session SID that’s how Windows says “this session’s stuff can touch this session’s desktop.” An S4U token is created as a network-style logon, and network logons carry no logon SID. So our elevated process had no ticket to the desktop it was trying to draw on, and Windows gave it a station it could technically own but nobody could see.
The fix is to borrow. The agent already knows the interactive user’s session; it pulls that session’s token with WTSQueryUserToken, reads the interactive logon SID out of it, and injects that into the S4U token via the same pLocalGroups list it used for the Administrators SID. Now the elevated token carries the one SID the desktop’s DACL is looking for, and the window renders like any other.
Bug two: the time-box has a hole MSIX walks straight through
Time-boxing is a Job Object. We launch the elevated process inside a job created with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, assigned before the process runs. Revoke the grant - manually, or when it expires - and closing that job handle takes the whole process tree with it. That’s the clean, kernel-enforced version of “pull them from the group,” except it actually kills the descendants instead of leaving them standing.
Then we tested it against a modern app and the revoke did nothing. The elevated process was gone; the app it launched was still running.
The culprit is out-of-process activation. A lot of what users launch today - Store/MSIX apps, some shell verbs, anything that goes through a broker - doesn’t start as a child of your process. It asks a system service to start it, and that service is the actual parent. So the app is never a descendant of our job, and KILL_ON_JOB_CLOSE never had it in scope. The job closes, the escapee keeps running with the admin token we handed out.
You can’t fix this by chasing parent-child links, because the link deliberately isn’t there. What every process minted from our grant does share is the logon session - the AuthenticationId LUID that S4U stamped on the token, identical on every process that traces back to that grant, even the ones the broker started. So revoke does two things now: it closes the job (kills the tree that stayed in scope), and it sweeps the logon session - enumerate processes, read each token’s AuthenticationId, and terminate any that carry our grant’s LUID. The escapee gets caught by the session sweep even though it dodged the job.
The one rule you do not get wrong here: SYSTEM’s own logon session is a fixed LUID (0x3E7), and it must never be swept. We measured a real escapee’s session against SYSTEM’s before we let the sweep touch anything. Kill the wrong LUID and you’ve taken out the machine, not the grant.
Bug three: domain accounts need a different S4U
MSV1_0_S4U_LOGON - the package above - mints tokens for local accounts via NTLM’s local authority. Point it at a domain account and it can’t, because that account’s authority is a domain controller, not the local SAM. Domain accounts need Kerberos S4U (KERB_S4U_LOGON) instead. So the launcher picks the package by the shape of the account it’s handed: a UPN or a name whose domain isn’t this machine goes through Kerberos; a local account goes through MSV1_0. Same injection of the Administrators SID and the borrowed logon SID on top; just the right authority underneath. Domain-joined fleets were the whole point, so this one wasn’t optional.
What it is, and what it isn’t
Let me be straight about the limits, because “elevation” is a word that invites overclaiming.
For the window the grant is open, this is real local admin. A determined user could do real damage in that window, the same as they could if you’d added them to the group. We did not invent a safe kind of admin; there isn’t one.
What we changed is everything around the window. The grant is scoped to one program you launch, not a blanket session. It’s time-boxed and enforced by the agent on the box, so it expires on schedule even if the box can’t reach our server. It’s revocable to zero on demand, and revoke kills both the job tree and anything that escaped it through the logon-session sweep - not “removed from the group and still admin until logoff,” but actually gone in seconds. It never writes to the Administrators group, so there’s no membership to forget, no SAM change to replicate, and no forty-people-nobody-can-explain in next quarter’s audit. And every grant is logged with who asked, who approved, what program, and when it ended.
This is the version of “give them admin for a minute” I wanted at my old job and never had: one where the minute is real, the blast radius is one program, and the cleanup isn’t a manual step somebody skips. The OS was willing to do it the whole time. It just makes you assemble it from S4U, a job object, a borrowed SID, and a session sweep - and get all four right at once.
Time-boxed admin elevation is part of RFF, free for your first 100 endpoints. Grant a specific program for a set window, watch it in the audit log, and revoke the whole tree with one click - getrff.com.