Chapter 07Lesson 05~50 minutes

Service Accounts and Automation Identities

Design, create, constrain, inventory, and retire Linux service accounts and automation identities using least-privilege boundaries.

Service accountsAutomationHands-on lab

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.

Runtime identity

Constrains local processes

A dedicated UID/GID separates files and signals from unrelated services.

Automation identity

Authenticates to other systems

Keys, tokens, workload identity, or certificates should be scoped and rotated outside source code.

Ownership record

Makes lifecycle accountable

Someone must review access, respond to failures, rotate credentials, and retire the identity.

2. Design the identity before creating it

DecisionPreferred questionSafer default
UID classIs this a local system service?Distribution-managed system UID allocation
Login shellDoes a person need an interactive shell?nologin or equivalent
PasswordIs password login explicitly required?Locked/unset password
Home/stateWhere does durable service state belong?Purpose-specific directory with narrow ownership
GroupsWhich shared resources are required?No supplementary groups unless justified
Remote credentialsHow will rotation and revocation work?Workload identity or scoped secret manager

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

Service identity and credential boundaries
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.

Preferred pattern

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
A dedicated UID is only one layer

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.

01Scope

Limit repositories, environments, APIs, paths, commands, and network destinations.

02Isolate

Use ephemeral runners or clean workspaces to prevent cross-job credential and artifact leakage.

03Issue

Prefer short-lived credentials obtained at runtime over static keys stored on disk.

04Observe

Log identity use, target resources, failures, unusual timing, and privilege changes.

05Revoke

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.

Next chapter

Process Model, PIDs, Parents, and /proc

Chapter 8 follows these identities into running processes and examines parent-child relationships, kernel process state, and the /proc interface.

11. Further reading

  • useradd(8), nologin(8), passwd(1), and runuser(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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.