---
id: 40
title: "No anonymous \"system\" actor"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [security, auth, infra, operations]
summary: "If \"system\" appears as an actor in your audit log, attribution is already broken. Every automated action — cron job, cleanup task, migration, agent — runs as a named identity with its own credentials and scope, so \"who did this?\" has an answer and revocation is surgical."
supersedes: null
superseded_by: null
aliases: []
references:
  - id: spiffe
    title: "SPIFFE — Secure Production Identity Framework for Everyone"
    url: https://spiffe.io/
    abstract: "The CNCF standard for workload identity: every workload gets its own verifiable, named identity (a SPIFFE ID) and short-lived credentials, instead of shared secrets or ambient platform trust."
---

## TL;DR

Every automated action runs as a **named principal**. Cron jobs, cleanup tasks, migrations,
archival, schedulers, agents — each one:

- has its **own identity**, named for what it does (`journal-archiver`, `stale-lock-reaper`), with
  its **own credentials and minimal scope**;
- appears **by name as the actor** in every audit record it produces — and when it acts on behalf
  of a user, the record names **both** the actor and the principal;
- has a **human owner** and can be disabled, re-scoped, or rotated **without touching anything
  else**.

Actor values like `system`, `root`, `admin`, or `cron` are banned — reject them at write time. The
test: from any audit record, can you get to the exact job and code that performed the action, and
turn that one thing off without turning off everything? If the answer is "it was the system," you
don't have an audit log; you have a diary that says *something happened*.

## Context

The anonymous "system" user is never designed; it accretes. It starts as a convenience — the
migration runner needs a user id, the nightly cleanup needs credentials, so someone creates
`system` with broad permissions and moves on. Two years later every privileged background job runs
as it, its credentials are embedded in a dozen places, nobody can rotate them without an outage,
and its permission set is the union of everything any job ever needed — which is to say,
everything.

The audit cost is the worst part. An audit log exists to answer *who did this, with what authority,
and what else can that identity do?* The `system` actor answers none of these. "System deleted
3,000 rows at 03:14" tells you a machine was involved; it doesn't tell you **which** job, **which**
code path, **which** schedule, or **what to revoke**. Investigations stall exactly there.

We solved this on the human side long ago: nobody would accept a shared `employee` login, because
attribution, scoping, and revocation all demand individual identities. Machine actors deserve —
and with [workload identity](ref:spiffe) now standard, can cheaply have — the same discipline.
And the pressure is rising: background automation increasingly means LLM agents acting on
schedules and triggers. A fleet of agents all acting as "the system" is unaccountable in a way
that compounds; an agent acting under its own name, on behalf of a named user, is just another
principal in the log.

This is the machine-side twin of [ZFN-8](/zfn/8-dont-speak-for-anonymous-people/): don't hide
behind anonymous "people", and don't let your automation hide behind an anonymous "system".

> [!aside] The password nobody will rotate
>
> The recurring artifact is the shared service credential that predates everyone on the team,
> granted permissions nobody can enumerate, used by jobs nobody has inventoried — so rotating it is
> treated as a change-freeze-level event. That fear is the system telling you the identity is
> doing too many jobs.

## Recommendation

**One identity per automation.** Each job, scheduled task, worker, or agent gets its own identity,
named for its function. Naming is not cosmetic: the name is what shows up in the audit log, the
permission policy, and the incident channel at 3 a.m.

**Own credentials, minimal scope.** Issue each identity its own credentials — short-lived and
federated, per [ZFN-9](/zfn/9-no-long-lived-cloud-keys/), through one platform mechanism, per
[ZFN-5](/zfn/5-platform-workload-identity-service/) — scoped to what that automation actually
does. The archiver can write to the archive; it cannot touch user tables. Least privilege is only
real when identities are this granular.

**Actor on every mutation; principal when delegated.** Every state change records the named actor.
When automation acts on a user's behalf, record both: the actor (the automation) and the principal
(the user). Delegation is explicit and audited — an automation never silently *becomes* a user.

**Ban the strings.** Reject `system`, `root`, `admin`, `cron`, and friends as actor values at the
API or storage boundary, not in a code-review checklist. If the write path accepts an anonymous
actor, you will eventually have one.

**Every identity has an owner.** Keep a registry: automation identity → what it does → which team
or human owns it → where its runbook lives. An identity nobody owns is the shared `system` user
waiting to re-form.

**Audit symmetrically.** Scheduled runs that fire, fail, or are skipped are attributed events too.
"The reaper didn't run last night" should be answerable from the same log as "the reaper deleted
this."

## Consequences

**Easier:**

- Forensics: every audit record resolves to a specific workload, its code, and its owner. "Who did
  this?" stops being a research project.
- Revocation and rotation are surgical — disable or re-key one automation without a change freeze.
- Least privilege becomes enforceable, because each policy is scoped to one job's actual needs.
- A misbehaving automation (or agent) can be killed by name, immediately, with known blast radius.

**Harder:**

- Provisioning an identity per job is real work — make it a one-liner in your infrastructure
  templates, or people will share identities out of friction.
- More identities to inventory and govern. Federated, short-lived credentials ([ZFN-9](/zfn/9-no-long-lived-cloud-keys/))
  keep this from becoming a key-management burden; a registry keeps it from becoming sprawl.
- Some platforms still make fine-grained machine identities awkward; budget for the seam.

**New obligations:**

- The actor field is mandatory and validated on every mutation; delegated actions carry actor
  *and* principal.
- The automation-identity registry exists, has owners, and is kept current — an unowned identity
  is treated as a finding.

## References

- [ZFN-8](/zfn/8-dont-speak-for-anonymous-people/) — the human-side twin: don't attribute
  positions to anonymous "people"; don't attribute actions to an anonymous "system".
- [ZFN-5](/zfn/5-platform-workload-identity-service/) — the one platform mechanism that makes
  per-workload identities cheap.
- [ZFN-9](/zfn/9-no-long-lived-cloud-keys/) — federated, short-lived credentials, so many
  identities don't mean many static keys.
- [SPIFFE](https://spiffe.io/) — the CNCF workload-identity standard: named, verifiable identities
  per workload.

## Changelog

- **2026-06-12**: First published as a Field Note.
