Service Accounts and Automation Identities
Design, create, constrain, inventory, and retire Linux service accounts and automation identities using least-privilege boundaries.
Learning objectives
By the end of this lesson
- Distinguish human accounts from service, automation, CI-runner, and emergency identities.
- Create a locked, non-interactive system account with explicit ownership and filesystem boundaries.
- Run a process as a service identity and verify its effective access.
- Identify credential, home-directory, shell, group, and lifecycle risks for non-human accounts.
- Build an inventory that records ownership and purpose without collecting secrets.
1. Non-human identities still need owners
A daemon, deployment agent, backup process, monitoring collector, CI runner, or scheduled job needs an identity so Linux can constrain its processes and files. Calling it a “service account” does not make it safe. Every non-human identity needs a documented purpose, a responsible team, an approved authentication mechanism, a minimum set of resources, a rotation path, monitoring, and a retirement condition.
Constrains local processes
A dedicated UID/GID separates files and signals from unrelated services.
Authenticates to other systems
Keys, tokens, workload identity, or certificates should be scoped and rotated outside source code.
Makes lifecycle accountable
Someone must review access, respond to failures, rotate credentials, and retire the identity.
2. Design the identity before creating it
nologin or equivalent
System account UID ranges and home conventions vary by distribution.
Use useradd --system or package/service-management
mechanisms rather than choosing a low UID manually. A non-login
shell blocks conventional shell entry but is not a sandbox; any
process already running as that UID can still read and modify
resources permitted to it.
3. Separate local runtime identity from remote credentials
flowchart TD O["Human owner and approved change"] --> M["Service manager or scheduler"] M --> P["Process runs as dedicated UID/GID"] P --> F["Owned state and approved shared files"] P --> S["Secret or workload-identity provider"] S --> R["Scoped remote API or platform access"] P --> L["Logs, metrics, and audit evidence"] R --> V["Rotation and revocation lifecycle"]
The local UID controls host access. A cloud role, API token, SSH key, registry credential, or Kubernetes service account controls remote access. Do not confuse them. Removing the Linux account does not revoke a token stored elsewhere, and revoking a cloud credential does not stop a local process from reading files.
Use short-lived, automatically issued credentials bound to the workload when the platform supports them. Long-lived secrets in home directories, shell profiles, repositories, or CI variables require stronger inventory and rotation controls.
4. Create a constrained service account
The exact nologin path varies. Resolve it first. The
following commands create a lab system identity, a state directory,
and a read-only executable owned by root. The service account owns
only its state directory.
service_user=da-report
state_dir=/var/lib/da-report
program=/usr/local/libexec/da-report
nologin_shell=$(command -v nologin || true)
if [[ -z $nologin_shell ]]; then
printf 'nologin command not found; inspect /etc/shells and local policy.\n' >&2
exit 1
fi
if getent passwd "$service_user" >/dev/null; then
printf 'Refusing to reuse existing account: %s\n' "$service_user" >&2
exit 1
fi
sudo useradd --system --user-group --home-dir "$state_dir" --create-home --shell "$nologin_shell" "$service_user"
sudo install -d -o "$service_user" -g "$service_user" -m 0750 "$state_dir"
program_tmp=$(mktemp)
cat > "$program_tmp" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
umask 027
state_dir=/var/lib/da-report
printf 'generated=%s uid=%s gid=%s\n' "$(date -Is)" "$(id -u)" "$(id -g)" > "$state_dir/last-run.txt"
SCRIPT
sudo install -o root -g root -m 0755 "$program_tmp" "$program"
rm -f "$program_tmp"
# The account has no usable password and cannot start a normal login shell.
sudo passwd --status "$service_user"
getent passwd "$service_user"
id "$service_user"
The program is root-owned so the service identity cannot replace the code it executes. The state directory is service-owned so the process can write its output. In production, use package management, configuration management, or a service unit with additional hardening rather than ad hoc installation.
5. Run as the service identity and verify boundaries
service_user=da-report
state_dir=/var/lib/da-report
program=/usr/local/libexec/da-report
# Run exactly the program as the non-human identity.
sudo -u "$service_user" -- "$program"
# Verify process-produced state and ownership.
sudo cat "$state_dir/last-run.txt"
sudo stat -c 'path=%n owner=%U(%u) group=%G(%g) mode=%A' "$state_dir" "$state_dir/last-run.txt" "$program"
# Negative tests: the service must not rewrite its executable or /etc.
if sudo -u "$service_user" test -w "$program"; then
printf 'Service can modify its program: unsafe.\n' >&2
exit 1
fi
if sudo -u "$service_user" test -w /etc; then
printf 'Service can write /etc: unsafe.\n' >&2
exit 1
fi
# Demonstrate that the non-login shell rejects conventional shell access.
# The exact message depends on the nologin implementation.
sudo -u "$service_user" -- "$(getent passwd "$service_user" | cut -d: -f7)" || true
Also consider filesystem permissions, ACLs, capabilities, sudo rules, systemd hardening, namespaces, network policy, secrets, resource limits, and mandatory access control. Later chapters cover these controls in context.
6. CI runners and deployment automation
Automation identities often accumulate broad repository, artifact, cloud, and production access because one pipeline must perform many steps. Split identities by environment and function where practical: build should not automatically deploy to production; read-only verification should not possess write credentials; pull-request jobs should not receive protected production secrets.
Limit repositories, environments, APIs, paths, commands, and network destinations.
Use ephemeral runners or clean workspaces to prevent cross-job credential and artifact leakage.
Prefer short-lived credentials obtained at runtime over static keys stored on disk.
Log identity use, target resources, failures, unusual timing, and privilege changes.
Automate credential expiry and account cleanup when a project or integration ends.
7. Hands-on lab: inventory and retire the lab service
Create a non-secret ownership record, verify the identity has no supplementary groups beyond its primary group, capture owned paths, and remove the lab resources.
lab="$HOME/devops-academy/linux/chapter07/lesson05"
mkdir -p "$lab"
report="$lab/service-identity-inventory.txt"
service_user=da-report
state_dir=/var/lib/da-report
program=/usr/local/libexec/da-report
{
printf 'identity=%s\n' "$service_user"
printf 'purpose=Chapter 7 isolated reporting lab\n'
printf 'owner=%s\n' "$(id -un)"
printf 'review_date=%s\n' "$(date -d '+30 days' +%F)"
printf '\n=== account ===\n'
getent passwd "$service_user"
id "$service_user"
sudo passwd --status "$service_user"
printf '\n=== managed paths ===\n'
sudo stat -c 'path=%n owner=%U(%u) group=%G(%g) mode=%A' "$state_dir" "$program"
} > "$report"
chmod 600 "$report"
less "$report"
# Cleanup: stop related processes first if any exist.
sudo pkill -u "$service_user" 2>/dev/null || true
sudo rm -f "$program"
sudo userdel --remove "$service_user"
# Confirm retirement.
if getent passwd "$service_user" >/dev/null; then
printf 'Account still resolves after cleanup.\n' >&2
exit 1
fi
Verification checklist
8. Common mistakes
Sharing one account among unrelated services
Shared UIDs erase isolation and make ownership, revocation, and incident attribution harder.
Giving a service an interactive shell
Most daemons do not need one. A non-login shell reduces accidental and unauthorized interactive use.
Letting the service rewrite its code
A compromised process can persist by replacing executables, scripts, plugins, or configuration loaded with higher trust.
Storing long-lived secrets in the home directory
Backups, support bundles, workspace reuse, or permissive modes can expose credentials.
Removing the UID but not remote access
Cloud roles, tokens, deploy keys, certificates, and registry credentials must be revoked separately.
9. Knowledge check
Question 1. Why should a service program normally be root-owned while its state directory is service-owned?
Question 2. What does a nologin shell protect
against?
Question 3. Why are short-lived workload credentials preferable to static tokens?
10. Summary
Service and automation accounts are security principals with real lifecycle obligations. Give each workload a dedicated identity where useful, disable interactive login by default, lock password authentication, protect executable code, grant write access only to required state, separate local UID access from remote credentials, observe use, and retire every associated resource.
11. Further reading
-
useradd(8),nologin(8),passwd(1), andrunuser(1). -
systemd documentation for
User=,Group=,DynamicUser=, and service sandboxing. - Your CI/CD platform's workload identity, protected secret, and runner-isolation guidance.
- Organizational non-human identity ownership and credential-rotation standards.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.