Get listed

Insider threats: named sudo, least privilege, no shared root

A shared coral master key on a common hook.

An insider threat is a person who already passed your login, using access you granted.

That can be a departing admin, a contractor with a shared role, or a compromised staff account. Network zero-trust does not help if the object check is ‘any authenticated user.’

The usual mistake is a briefing about bad employees and no log of who exported the customer table. Detection starts with privileged actions you can query.

This page is the team CISA tells you to assemble, and the technical checks that make an insider move visible.

CISA published Assembling a Multi-Disciplinary Insider Threat Management Team on 28 January 2026. The release names two shapes: a calculated act, and an unintentional mistake.That is scare copy. The control is a named account, a narrow grant, and a log that leaves the host.

This page is for operators and the engineers who write the grants. It is not a HR investigation playbook. Pair it with Ubuntu host hardening for the sshd and package baseline, with the secure coding checklist for object grants in the product, and with session management when the insider is a stolen cookie instead of a shell. The input validation guide is the fourth sibling when a privileged form still takes raw SQL from a support tool.

Privilege is the surface, not motive

Most of the damage this page can close is a grant that was wider than the task. A contractor with the deploy role, an engineer with the production DB password, a support tool that runs as the table owner. Intent is a later question for people who are not this page. CISA’s January 2026 note puts negligence next to malice on purpose. The Verizon 2026 DBIR, in the third-party cloud section calls missing least privilege on users and service accounts a pervasive issue, and says a good number of 2025 cloud incidents reduce to that plus missing MFA and sloppy rotation. I am not citing a vendor UEBA brochure or a $307,111 per-employee figure.

The product side is the same as any other caller. A staff admin token that can GET /users/:id without a tenant check is an insider path that looks like IDOR. Fix the query. Do not install a keylogger. Do not write a mole-hunting policy in the README. Name the role, name the action, and log the privileged read.

WhoGetsDoes not get
aliceSSH + listed sudoroot password
app_readSELECT on app tablesSUPERUSER
supporttime-boxed rolewiki root
cideploy to one envprod shell

No shared root, no shared admin

PermitRootLogin no is the sshd line. Each person has a user and a key or an IdP hop. Elevation goes through sudo so the invoking user is in the log. A shared ubuntu password, a shared ec2-user key in the team vault, and a root password in the onboarding doc are the same bug. Offboarding then means rotating a secret everyone still needs. 8organicbits said that in 2022. It is still the cheap test: can you drop one person without rotating the rest?

# /etc/ssh/sshd_config.d/10-named-users.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey

Reload sshd on a host you admin. Keep a console or out-of-band path that is not SSH before you lock the last key. The Ubuntu hardening page is the rest of banner, updates, and unattended upgrades. This page only needs the login to be a person.

CI is the other shared-admin trap. A long-lived deploy key in a group vault, a single root kubeconfig in Slack, or a cloud console login that five people know, fails the same offboarding test. Prefer an OIDC hop from the repo to a role that can deploy one environment. A human who needs a shell still uses alice, not the pipeline identity. When a contractor ends, you drop one IdP group. You do not rotate a password the remaining on-call still needs at 03:00.

# /etc/sudoers.d/alice visudo -f
Cmnd_Alias APP_RESTART = /bin/systemctl restart app.service, \
 /bin/systemctl status app.service
alice ALL=(root) APP_RESTART
Defaults:alice logfile="/var/log/sudo-alice.log"

APP_RESTART is the named grant. It is not ALL=(ALL) NOPASSWD: ALL. A full root shell is a break-glass ticket, not a default. sudo -s after a wide grant deletes the command trail. If Alice needs a root shell, use a session recorder you already run, or skip the shell and give her one more command in the alias. I am not recommending a specific commercial recorder. I am saying a shared root shell with no name on it is the hole.

# Named fallback: time-bounded break-glass, still a person
# /etc/sudoers.d/break-glass visudo -f
Cmnd_Alias BREAK_GLASS = /usr/local/sbin/start-break-glass
%break-glass ALL=(root) BREAK_GLASS

start-break-glass is a script you own that opens a ticket, prints the actor, and only then starts a recorded root shell. If you do not have that script yet, do not replace it with a wiki password. Keep Alice on APP_RESTART until the script exists. A %break-glass group you can empty on Friday is still better than one root hash everyone has memorized.

Grant the action, not the role nickname

Least privilege is a boring list. The app connects as app_read. Migrations connect as app_migrate from CI, not from a laptop. Humans who need a one-off select use a break-glass role that expires. SUPERUSER and table ownership stay off the application path.

-- run as a migration owner, once
CREATE ROLE app_read LOGIN PASSWORD NULL;
GRANT CONNECT ON DATABASE app TO app_read;
GRANT USAGE ON SCHEMA public TO app_read;
GRANT SELECT ON invoices, customers TO app_read;
GRANT INSERT ON staff_audit TO app_read;
REVOKE CREATE ON SCHEMA public FROM app_read;
REVOKE ALL ON invoices FROM PUBLIC;
REVOKE DELETE, UPDATE ON staff_audit FROM app_read;

The app process uses a certificate or a vault-issued password for app_read. It does not use the postgres role. A support engineer who needs to read one invoice uses the product’s admin screen, which still hits an object check, or a time-boxed SET ROLE you can see in pg_stat_activity and in the log. Do not hand out the table-owner URL in Slack.

On the application side, a staff flag in a JWT is not a grant on every row. Scope the query the same way you would for a customer. The checklist is the longer object list. A shared “admin” password for the back office is the web version of shared root. Give each operator a user. Put MFA on that user. Log the privileged read of a customer record as its own event, not as a generic 200.

async function loadStaffInvoice(pool, staff, invoiceId) {
 if (!staff.canReadBilling) return null;
 const { rows } = await pool.query(
 `SELECT id, memo, amount_cents
 FROM invoices
 WHERE id = $1`,
 [invoiceId]
 );
 await pool.query(
 `INSERT INTO staff_audit (actor, action, object_id)
 VALUES ($1, 'invoice.read', $2)`,
 [staff.id, invoiceId]
 );
 return rows[0] || null;
}

loadStaffInvoice is the named privileged read. It still has a capability check. It writes staff_audit in the same request. If the audit insert fails, fail the read. A support tool that swallows that error is how a dump goes unnoticed. I would rather the page 500 than silently skip the row.

Ship the audit before someone can delete it

A log that lives only on the box is a suggestion. Root can truncate it. CISA’s 28 January 2026 release already treats detection as part of the job, not a later add-on. The control is still: privileged commands and privileged data reads leave the host as they happen.

# /etc/audit/rules.d/app.rules then augenrules --load
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d -p wa -k sudoers
-w /etc/ssh/sshd_config.d -p wa -k sshd
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudo -k sudo-exec

Forward auditd, sshd, and the app staff_audit table to a store the production role cannot DELETE. journald on the same disk is not that store. A second account, an object-lock bucket, or your existing SIEM sink is. I will not name a product. I will say the app role must not be the owner of the audit table. INSERT only. A nightly job under a different role expires rows after a retention you can spell, 90 days or a year, not “forever” and not “until the disk fills.”

A person, a listed command, a copy of the event off the box. Shared root skips the name.
alice
 ssh (key, not root)
 |
 v
 sudo APP_RESTART
 |
 +--> local sudo.log
 |
 +--> auditd --forward--> log host
 |
 app cannot DELETE

Session recordings of a break-glass shell are a later control. Get the named login and the off-host sudo line first. A UEBA product on top of a shared root password will still attribute the session to “root”. That is the opposite of the 8organicbits offboarding test.

Prove who can become root

You are listing grants on hosts and databases you admin. You are not attacking a coworker.

  1. sshd -T | rg -n "permitrootlogin|passwordauthentication" must print no and no.
  2. getent passwd root may exist. grep ^root /etc/shadow should be locked (! or *) if you never log in as that user on the console.
  3. sudo -U alice -l must list APP_RESTART and must not list ALL.
  4. In Postgres, SELECT rolname, rolsuper FROM pg_roles WHERE rolname IN ('app_read','postgres'); app_read is false. The app DSN user is app_read.
  5. Pick one privileged product action. Confirm staff_audit grew a row, and that app_read cannot DELETE that row.
  6. Offboard a test account. Confirm its sudoers drop and its IdP group drop, and that no shared password needed a rotate.
sshd -T | awk '/^permitrootlogin|^passwordauthentication/'
sudo -U alice -l
psql -c "SELECT rolname, rolsuper FROM pg_roles WHERE rolname = 'app_read'"
psql -c "SELECT grantee, privilege_type FROM information_schema.role_table_grants
 WHERE table_name = 'staff_audit'"

If app_read can DELETE or UPDATE on staff_audit, the page failed. INSERT on that table is the only write that role should have. If alice has NOPASSWD: ALL, the page failed. If the onboarding doc still has a root password, delete that paragraph before you add another audit rule.

Questions we keep getting

Is sudo useless if Alice has a full shell?

A wide grant is a named full shell. You still know it was Alice. Shared root does not. Narrow the alias. Keep the name. A NOPASSWD: ALL line is a ticket to shrink, not a reason to go back to one password.

Do we need a vendor UEBA to start?

No. Named accounts, a short sudoers list, and logs that leave the host are the start. CISA’s 2026 team graphic is about who talks when something looks wrong. It is not a shopping list. Buy tooling after those three exist.

What about a break-glass root on the console?

Keep one, offline, in a sealed path two people can open, with a ticket when it is used. Do not put that secret in the same vault the whole on-call channel can copy. Rotate it when it is used. Daily work stays on named sudo.