Nobody should have permanent root. JitAccess is a just-in-time privileged access broker for Linux hosts: you request elevated access with a reason, someone else approves it, and it removes itself when the window closes.
Ask any mid-size company who has permanent sudo on production. The honest
answer is usually "about twelve people, because revoking it breaks on-call."
That's how it happens. Someone needs root at 3am during an incident, gets it, and nobody ever takes it away — because taking it away is a task no one is assigned and no one is rewarded for. Access accretes. It never drains.
The cost of that shows up in three different places:
| Where it hurts | What it looks like |
|---|---|
| Breach blast radius | One phished laptop belongs to someone with standing root on every host. The initial compromise and the total compromise are the same event. |
| Audit | SOC 2, ISO 27001 and PCI all ask "who has privileged access and why?" Answering it with a stale spreadsheet is how findings get written. |
| Offboarding | Someone leaves. HR disables their SSO. Their sudo group membership on 40 hosts is nobody's job to notice. |
- "We'll review access quarterly." A review is a snapshot. Access drifts
the day after, and the reviewer has no context to judge whether
alicestill needswheel. - "Just file a ticket." Ticket queues have no notion of expiry. The ticket that granted access is closed the moment access is granted — the undo is never tracked.
- "We have a shared break-glass account." Now the audit log says
root did itand you cannot tell which human that was.
Every one of these fails for the same reason: they depend on a person remembering to undo something. People don't, especially not at 3am.
It keeps the shape of the ticket everyone already understands — request, approve, close — and makes the closing part automatic and enforced.
request ──────approve──────> active ──────(clock)──────> expired
│ (by (privilege no human privilege
│ someone applied) involved removed
│ else) )
│
├──deny──> denied
│
└── (while active) ──revoke──> revoked
Four ideas do the work:
- Approval is the grant. There is no "approved but not yet active" state, so there's never a window where privilege has been blessed but nobody is counting it.
- Every grant has an expiry. Not a reminder, not a policy — a timestamp, after which a reaper removes the access whether anyone is watching or not.
- A requester can never approve themselves. Separation of duties, enforced in code and not overridable by config.
- Refusals are recorded too. A blocked self-approval is a control working as designed, and it's exactly the event an investigator wants to find later.
| Before | With JitAccess | |
|---|---|---|
| Who has root right now? | "Let me check the wiki" | jitctl list |
| How long will they have it? | Forever | 30 minutes |
| Who approved it? | Nobody remembers | In the audit log, signed into a hash chain |
| Why did they need it? | Lost in Slack | Required field, stored with the grant |
| What happens when they leave? | It lingers | It already expired |
| Proving it to an auditor | Screenshots | jitctl verify |
The shift is from access as a state you're in to access as an event that happened. Standing privilege becomes a thing that has to be justified each time, which is the only version of least-privilege that survives contact with an on-call rotation.
- Small platform/infra teams who need defensible access control but can't buy Teleport or CyberArk.
- Companies heading into a SOC 2 / ISO 27001 audit who need to show privileged access is time-bound and reviewed.
- Anyone running production on a handful of Linux hosts where "everyone is
in
wheel" is the current design.
Concretely: an on-call engineer requests 30 minutes of host-sudo citing an
incident number, the secondary approves from their phone, the engineer fixes
the disk, and the access is gone before the incident review starts — with a
record that answers every question that review will ask.
These are decisions, and each one is a constraint on the code:
- Standard library only. Every dependency in a security tool is code you are trusting on someone else's behalf.
- All privilege-changing code lives behind one interface, so the part that could escalate privilege is small enough to read in one sitting.
- The tool never edits
/etc/sudoers. A bug in that code would be a root escalation — precisely what this exists to prevent. An administrator installs a drop-in once; JitAccess only moves people in and out of groups. - The audit log is append-only and hash-chained. Tamper-evident, not tamper-proof — an honest claim we can actually back.
- Nothing is marked done until it has been run.
go build -o jitctl ./cmd/jitctlRoles live in /etc/jit-access/config.json (override with JIT_ACCESS_CONFIG).
See examples/config.json:
{
"roles": [
{"name": "dba", "group": "jit-dba", "max_duration": "4h", "approvers": ["alice", "bob"]}
]
}The group and the approver list live in the same record so they cannot drift apart. A config that is missing, malformed, or names a role with no approvers is refused at startup rather than silently defaulted.
jitctl request -role dba -duration 1h -reason "quarterly migration rollback"
jitctl list
jitctl approve req-1787421806216733179 # never by the requester
jitctl deny req-...
jitctl revoke req-... # ends an active grant early
jitctl verify # audit log against its anchor
jitctl reap # expire what is due; run from cron
jitctl help # the manual: model, files, config
jitctl help reap # long form for any one commandjitctl help explains the model, every command, where state lives, the config
format and the exit codes. Every subcommand also takes -h, which prints the
same page plus its real flags.
State lives in /var/lib/jit-access (0700; override with
JIT_ACCESS_STATE_DIR): requests.json, audit.jsonl, anchor.json, and a
.lock that serializes concurrent commands.
Feature complete — 2026-08-23. All commands work against real state on disk, and the privilege path is verified as root against real Unix groups.
Milestone 1 — the core model
-
domain— the Request type and its state machine -
policy— role definitions, duration caps, approver rules -
store— crash-safe persistence (atomic replace + fsync) -
audit— the hash-chained log, plus an out-of-band anchor that detects truncation and wholesale rewrite -
config— roles, groups, durations and approvers from one file -
lockfile— flock, so two commands cannot interleave a read-modify-write -
jitctl— the CLI that ties them together
Milestone 2 — real enforcement
- Unix group executor, behind the
Executorinterface - Verified as root: real grant, real revoke, real expiry via
reap - sudoers drop-in, validated with
visudo -c -
systemdtimer, so expiry doesn't depend on someone running a command
Milestone 3 — trustworthy identity
- Ship the audit head off-host, so the anchor is not on the disk it guards
- Approval notifications
Later
- AWS backend: STS session credentials instead of Unix groups
- Revoking group membership does not end sessions that are already open.
- The anchor sits beside the log, written by the same process. It catches tampering by anyone who is not root on this host; it is only a hard guarantee once the head leaves the machine.
A learning project, built deliberately rather than quickly. The things I'm here to actually understand:
- How privilege is really carried and revoked on a Unix host — groups, sudoers, and where the seams are
- Why time-boxing beats remembering, and what breaks when a session outlives its grant
- Modelling a security control as a state machine, so illegal transitions are unrepresentable rather than merely discouraged
- Building a record that stays trustworthy when the person holding the pen has a motive to edit it