Least privilege means granting a principal the operations required for a defined job, on the intended resources, for an appropriate lifetime. It is a testable policy rather than a reassuring label. Start with the service’s real launch identity and dependencies; an unused permission and a missing permission are different findings that need different responses.

Define the minimum in writing

Record the files the service reads, the directories it writes, its required network destinations, and its administrative operations. Include startup, logging, rotation, recovery, and shutdown. A service may legitimately need temporary storage and shared libraries even when its business data belongs in one directory. A useful inventory explains each access rather than declaring everything outside the data directory unnecessary.

For systemd units, User= and Group= choose identity, while filesystem permissions and sandboxing impose additional boundaries. ProtectSystem=strict makes most of the filesystem hierarchy read-only within the unit, with documented API-filesystem exceptions and possible writable exceptions. Read-only does not mean unreadable. Protecting confidential content requires separate access controls or visibility restrictions, such as an appropriate ProtectHome= policy.

Test the specific boundary

Different directives solve different problems. PrivateDevices= restricts device exposure, NoNewPrivileges= prevents gaining new privileges through execution, and RestrictAddressFamilies= restricts socket families. The last option is not a destination or port firewall. Kernel support, service-manager context, existing descriptors, and other settings affect what is enforced; a list of directive names is not proof of a complete sandbox.

A capability can be narrower than unrestricted root, but capabilities are not necessarily tiny permissions. Some cover broad administrative powers. Grant one only after checking its documented operations and whether a less powerful mechanism, such as file ownership, an ACL, or a separately constrained helper, fits the need. Test an intended operation and a forbidden neighboring operation under the actual service context.

Why broad access persists

Temporary incident grants commonly outlive the incident. Record an owner, reason, and expiry or review point so recovery access does not silently become routine authority. Compare the deployed bindings with the approved policy after changes. A configuration file in source control does not prove that the running workload uses it.

Credential lifetime is a separate control. A narrowly scoped token can still be misused while valid, and rotating a broadly scoped credential does not reduce its permissions. Prefer appropriately bounded credentials and avoid copying them into logs or runbooks. Review both what the credential permits and how it is revoked when the task or relationship ends.

When it breaks

In Kubernetes, wildcard verbs and resources grant broad access within the rule’s API groups and the binding’s scope. They do not automatically mean cluster administrator across every API group and namespace. Still, new matching resources or verbs can inherit permission without another policy edit. Check effective subjects, namespaces, resource names, and all additive grants before concluding that one narrow Role limits total access.

Observed non-use is evidence for review, not automatic permission removal. A recovery action may be essential but absent from an ordinary week. Conversely, an action used by one role does not justify granting it to every role. Keep usage keyed by identity and role, and obtain the operational reason for exceptions rather than treating a global used-action list as authorization.

Over-tightening can block legitimate deployments and encourage unsafe workarounds. Supply a narrow supported path and test it before removing access. Impersonation is itself permission-controlled; adding an impersonation flag does not magically create administrator rights. Keep emergency procedures accountable rather than relying on hidden shared credentials.

Check it quickly

Save this as privilege_audit.py and run python3 privilege_audit.py. The standard-library model replays seven synthetic requests and separately compares each role with its own synthetic review-window usage. It does not read credentials or change permissions.

ROLES = {
    "api-reader": {"articles:read", "health:read"},
    "deployer": {"articles:read", "articles:publish", "health:read"},
    "ops-admin": {"articles:read", "articles:publish", "secrets:read", "users:write"},
}

# (role, action, should_allow)
REQUESTS = [
    ("api-reader", "articles:read", True),
    ("api-reader", "articles:publish", False),
    ("api-reader", "secrets:read", False),
    ("deployer", "articles:publish", True),
    ("deployer", "secrets:read", False),
    ("ops-admin", "secrets:read", True),
    ("ops-admin", "users:write", True),
]

# Synthetic observations from an independent review window, per role.
USED = {
    "api-reader": {"articles:read", "health:read"},
    "deployer": {"articles:read", "articles:publish", "health:read"},
    "ops-admin": {"articles:read", "articles:publish"},
}


def allowed(role, action):
    return action in ROLES.get(role, set())


for role, action, expected in REQUESTS:
    decision = allowed(role, action)
    assert decision is expected, (role, action, decision)
    print(f"{role:10} {action:17} -> {'allow' if decision else 'deny'}")

for role, granted in sorted(ROLES.items()):
    unused = sorted(granted - USED[role])
    print(f"{role:10} granted={len(granted)} unused={len(unused)} {unused}")
    if role == "ops-admin":
        assert unused == ["secrets:read", "users:write"], unused

assert not allowed("unknown-role", "articles:read")
assert not allowed("api-reader", "unknown:action")
print("PASS: 7 requests match the matrix; ops-admin holds 2 unused grants")

Expect seven decision lines, three usage lines, and the final PASS line. The reader cannot publish or read secrets; the deployer cannot read secrets. The administrator has two grants absent from its review-window set. Unknown roles and actions are denied. The request fixtures and usage fixtures represent different inputs, so a permitted test request does not become a real usage observation.

Both revised language examples passed with Python 3.12.14 on Linux. These checks establish set-based decision behavior only. They do not exercise systemd confinement, Kubernetes authorization, admission policies, or token expiry. Production verification must test those actual enforcement points.

Further reading

Sources