Chapter 15Lesson 05250–330 min

Checkpoint Lab — Users, Roles, Privileges, Realms, Anonymous Access, User Tokens, and Least-Privilege Design

Implement and prove a least-privilege matrix for developer, CI publisher, consumer, and repository operator identities, rotate the CI credential, and remove every disposable security object.

Checkpoint labFour identitiesAuthorization matrixRotationEvidence

Learning objectives

  • Design a four-identity least-privilege matrix before implementation.
  • Implement local Community-compatible users/roles on a disposable repository topology.
  • Prove positive and negative authorization through direct and group endpoints.
  • Rotate the CI publisher credential and prove the old secret is invalid.
  • Produce an evidence packet and remove all disposable identity state.

Checkpoint scope. Use a dedicated disposable self-hosted Community instance. Reference baseline: Nexus Repository 3.95.0-07, Java 21, local H2 only for the small non-container learning instance or another supported lab database. Record the actual version/database/blob/edition. User Tokens, SAML, OIDC, HA, and other paid capabilities are not required.

1. Scenario: four responsibilities, four identities

Your artifact platform serves one small team. Developers and consumers retrieve artifacts through a Raw group. CI publishes to a hosted repository. A repository operator may inspect and edit the repository configuration but should not administer users or the entire Nexus instance. The security design must express those responsibilities without nx-admin.

Principal Client endpoint Required actions Explicitly not required
ch15cp-developer Group read endpoint Browse + read Publish, delete, repository admin, security admin
ch15cp-consumer Group read endpoint Read; optionally browse if the client/UI requires it Publish, delete, repository admin
ch15cp-ci Hosted publish endpoint + group read Hosted browse/read/add/edit; group read Delete, repository admin, security admin
ch15cp-operator Repository configuration/API Repository-admin browse/read/edit for disposable repos; content read as needed User/role management, nx-admin, wildcard security privileges

2. Preflight and before-state

export NX_URL="http://127.0.0.1:8081"
export LAB="$(mktemp -d)"
mkdir -p "$LAB/evidence" "$LAB/payload"
printf 'academy chapter 15 checkpoint\n' > "$LAB/payload/build.txt"
sha256sum "$LAB/payload/build.txt" > "$LAB/evidence/build.sha256"

export NX_USER="admin"
read -r -s -p 'Disposable Nexus admin password: ' NX_PASS; echo
export NX_AUTH_FILE="$(mktemp)"; chmod 600 "$NX_AUTH_FILE"
printf 'machine 127.0.0.1 login %s password %s\n' "$NX_USER" "$NX_PASS" > "$NX_AUTH_FILE"
unset NX_PASS

curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/status" > "$LAB/evidence/status.txt"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/system/license" > "$LAB/evidence/license.json" || true
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/anonymous" > "$LAB/evidence/anonymous-before.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/realms/active" > "$LAB/evidence/realms-before.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/users" > "$LAB/evidence/users-before.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/roles" > "$LAB/evidence/roles-before.json"

3. Predictions before mutation

Write at least these predictions into $LAB/evidence/predictions.txt before creating anything:

  1. An unauthenticated GET through the group will be denied while global anonymous access is disabled.
  2. The CI principal will be able to create/update the designated hosted asset but will be denied deleting it because no delete privilege is assigned.
  3. The repository operator will be able to inspect/edit the disposable repository configuration yet will be denied user-management endpoints because its role contains no security-management privileges.
  4. Changing the CI password will invalidate the old credential while leaving the CI role assignments unchanged.

4. Create disposable hosted and group repositories

Create academy-ch15cp-hosted as Raw hosted and academy-ch15cp-group as Raw group with the hosted repository as its only member. Use supported UI/REST operations and verify the final topology before creating roles. The group is the read endpoint; publication goes to hosted.

Capture repository configuration with the Repositories API. This evidence proves which resource names the generated repository privileges should reference.

5. Establish the anonymous baseline

On this dedicated lab instance, disable global anonymous access using Settings → Security → Anonymous Access and verify via GET /service/rest/v1/security/anonymous. Preserve the before-state so cleanup can restore it exactly. Do not alter global anonymous access on a shared Nexus server merely to satisfy this checkpoint.

6. Build the roles from outcomes, not job titles

Role ID Minimum privilege intent
academy-ch15cp-developer Group browse + read
academy-ch15cp-consumer Group read and only add browse if the chosen client/UI needs it
academy-ch15cp-ci Hosted browse/read/add/edit + group read; no delete
academy-ch15cp-operator Repository Admin privileges only for academy-ch15cp-hosted and academy-ch15cp-group required to inspect/edit configuration; no security-management privileges

After creating the repositories, filter the Privileges view for each exact repository name and select only the required generated privilege IDs. Do not use wildcard * repository names for this checkpoint.

7. Create four local users

Create ch15cp-developer, ch15cp-consumer, ch15cp-ci, and ch15cp-operator. Treat ch15cp-ci as the dedicated CI service account, a separate principal with its own credential lifecycle rather than a shared human login. Assign exactly one corresponding role to each. Set temporary passwords interactively and record only identity IDs/role assignments in evidence—never the secret values.

8. Create private credential files

make_auth () {
  user="$1"; var="$2"
  read -r -s -p "Temporary password for $user: " pass; echo
  file="$(mktemp)"; chmod 600 "$file"
  printf 'machine 127.0.0.1 login %s password %s\n' "$user" "$pass" > "$file"
  unset pass
  export "$var=$file"
}
make_auth ch15cp-developer DEV_AUTH
make_auth ch15cp-consumer CONSUMER_AUTH
make_auth ch15cp-ci CI_AUTH
make_auth ch15cp-operator OP_AUTH

export HOSTED_URL="$NX_URL/repository/academy-ch15cp-hosted"
export GROUP_URL="$NX_URL/repository/academy-ch15cp-group"

Windows/PowerShell equivalent. Keep the same security property: credentials belong in a temporary protected object/file, never a command argument. Use Get-FileHash -Algorithm SHA256 where Bash uses sha256sum. The Nexus endpoints and authorization expectations are unchanged.

9. CI publication and consumer reads

curl -sS --netrc-file "$CI_AUTH" --upload-file "$LAB/payload/build.txt"   -o "$LAB/evidence/ci-put.body" -w 'CI PUT %{http_code}\n'   "$HOSTED_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/ci-put.status"

curl -sS --netrc-file "$DEV_AUTH"   -o "$LAB/evidence/dev-read.txt" -w 'developer GET %{http_code}\n'   "$GROUP_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/dev-read.status"

curl -sS --netrc-file "$CONSUMER_AUTH"   -o "$LAB/evidence/consumer-read.txt" -w 'consumer GET %{http_code}\n'   "$GROUP_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/consumer-read.status"
sha256sum "$LAB/evidence/dev-read.txt" "$LAB/evidence/consumer-read.txt"   > "$LAB/evidence/consumer-hashes.sha256"

Compare the retrieved hashes with build.sha256. Byte equality proves the clients retrieved the same payload; it does not prove trusted provenance or vulnerability safety.

10. Negative authorization matrix

# Anonymous group read must be denied.
curl -sS -o /dev/null -w 'anonymous group GET %{http_code}\n'   "$GROUP_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/anon-group.status"

# Developer and consumer cannot publish.
for pair in "developer:$DEV_AUTH" "consumer:$CONSUMER_AUTH"; do
  name="${pair%%:*}"; auth="${pair#*:}"
  curl -sS --netrc-file "$auth" --upload-file "$LAB/payload/build.txt"     -o "$LAB/evidence/$name-put.body" -w "$name PUT %{http_code}\n"     "$HOSTED_URL/releases/$name-must-not-write.txt" | tee "$LAB/evidence/$name-put.status"
done

# CI must not administer repository configuration.
curl -sS --netrc-file "$CI_AUTH"   -o "$LAB/evidence/ci-repo-admin.body" -w 'CI repository-config GET %{http_code}\n'   "$NX_URL/service/rest/v1/repositories/raw/hosted/academy-ch15cp-hosted"   | tee "$LAB/evidence/ci-repo-admin.status"

# Operator may inspect repository config but must not list/manage users.
curl -sS --netrc-file "$OP_AUTH"   -o "$LAB/evidence/operator-repo.json" -w 'operator repository GET %{http_code}\n'   "$NX_URL/service/rest/v1/repositories/raw/hosted/academy-ch15cp-hosted"   | tee "$LAB/evidence/operator-repo.status"
curl -sS --netrc-file "$OP_AUTH"   -o "$LAB/evidence/operator-users.body" -w 'operator users GET %{http_code}\n'   "$NX_URL/service/rest/v1/security/users" | tee "$LAB/evidence/operator-users.status"

Exact status codes can vary around reverse proxies and unauthenticated challenges. The policy invariants are what matter: anonymous cannot read, developer/consumer cannot publish, CI cannot administer the repository, and the repository operator cannot administer identities.

11. Prove the CI role cannot delete

curl -fsS --netrc-file "$NX_AUTH_FILE"   "$NX_URL/service/rest/v1/assets?repository=academy-ch15cp-hosted"   > "$LAB/evidence/assets.json"
export ASSET_ID="$(python - <<'PYI'
import json, os
from pathlib import Path
p=Path(os.environ['LAB'])/'evidence/assets.json'
for item in json.loads(p.read_text()).get('items', []):
    if item.get('path') == 'releases/app/1.0.0/build.txt':
        print(item['id']); break
PYI
)"
test -n "$ASSET_ID"

curl -sS --netrc-file "$CI_AUTH" -X DELETE   -o "$LAB/evidence/ci-delete.body" -w 'CI DELETE %{http_code}\n'   "$NX_URL/service/rest/v1/assets/$ASSET_ID" | tee "$LAB/evidence/ci-delete.status"

curl -fsS --netrc-file "$DEV_AUTH"   "$GROUP_URL/releases/app/1.0.0/build.txt"   -o "$LAB/evidence/after-delete-test.txt"

If CI deletion succeeds, the checkpoint fails. Inspect the complete CI role set for an unintended delete or wildcard grant before doing anything else.

12. Rotate one credential and prove continuity

Change the password for ch15cp-ci through the Users UI. Create a new private netrc file, test both old and new credentials, and keep the role unchanged:

read -r -s -p 'New temporary password for ch15cp-ci: ' CI_NEW_PASS; echo
export CI_NEW_AUTH="$(mktemp)"; chmod 600 "$CI_NEW_AUTH"
printf 'machine 127.0.0.1 login ch15cp-ci password %s\n' "$CI_NEW_PASS" > "$CI_NEW_AUTH"
unset CI_NEW_PASS

curl -sS --netrc-file "$CI_AUTH" -o /dev/null -w 'old CI credential %{http_code}\n'   "$GROUP_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/ci-old-after-rotation.status"
curl -sS --netrc-file "$CI_NEW_AUTH" -o /dev/null -w 'new CI credential %{http_code}\n'   "$GROUP_URL/releases/app/1.0.0/build.txt" | tee "$LAB/evidence/ci-new-after-rotation.status"

The old secret should no longer authenticate while the new secret performs the same least-privilege job. This distinguishes credential lifecycle from authorization lifecycle.

13. Optional Pro extension: User Token without changing authorization

If—and only if—the disposable instance has a Pro license, you may enable User Tokens, generate one for ch15cp-ci, store its two parts in an approved temporary secret mechanism, and rerun the same matrix. The expected authorization must not change. Then delete/reset the token and verify it no longer works.

Do not enable “Require User Tokens for Repository Authentication” on a shared system. Enabling token expiration or resetting all tokens can break every build using those credentials.

14. Required evidence packet

Keep only non-secret evidence:

  • actual Nexus version/edition/runtime/database/blob assumptions;
  • anonymous settings before/during/after;
  • active realm IDs;
  • four user IDs and their assigned role IDs;
  • role privilege lists;
  • repository topology;
  • positive/negative HTTP status results;
  • payload SHA-256 comparisons;
  • old/new CI credential outcome (status only);
  • cleanup confirmation.
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/users" > "$LAB/evidence/users-final.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/roles" > "$LAB/evidence/roles-final.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/anonymous" > "$LAB/evidence/anonymous-final-before-restore.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/realms/active" > "$LAB/evidence/realms-final.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/repositories" > "$LAB/evidence/repositories-final.json"

15. Verification checklist

  • Developer: group read succeeds; publish/delete/admin actions fail.
  • Consumer: intended group read succeeds; write/admin actions fail.
  • CI: hosted publish + group read succeed; delete/repository-admin/security-admin fail.
  • Operator: intended repository configuration read/edit succeeds; user/role administration fails.
  • Anonymous group read fails during lab.
  • Old CI credential fails after rotation; new credential succeeds.
  • No identity has nx-admin, nx-all, or a wildcard repository grant unless deliberately required and documented—which this checkpoint does not require.

16. Cleanup and rollback

  1. Delete the four disposable users using supported Security UI/API operations.
  2. Delete the four custom roles.
  3. Delete academy-ch15cp-group first, then academy-ch15cp-hosted.
  4. If the optional Pro User Token extension was used, reset/delete the lab token before deleting its user.
  5. Restore anonymous access exactly to the captured pre-lab state.
  6. Confirm the user/role/repository names are absent from APIs.
  7. Remove all temporary credential files and the local workspace.
rm -f "$DEV_AUTH" "$CONSUMER_AUTH" "$CI_AUTH" "$CI_NEW_AUTH" "$OP_AUTH" "$NX_AUTH_FILE"
unset DEV_AUTH CONSUMER_AUTH CI_AUTH CI_NEW_AUTH OP_AUTH NX_AUTH_FILE NX_USER HOSTED_URL GROUP_URL ASSET_ID
rm -rf "$LAB"

17. Knowledge check

Why does the operator role exclude user-management privileges?

What does CI password rotation prove that a role test does not?

Why is the group endpoint part of every read test?

Would a Pro User Token change what ch15cp-ci is allowed to do?

What does Chapter 15 add to the production operating model?

18. Production operating-model addition and bridge to Chapter 16

This chapter adds an identity register and proof-driven access matrix to the artifact platform. Operators can now answer: who authenticated, through which source, which roles granted the action, which credentials are tied to automation, whether anonymous requests are intentional, and how stale identities are retired.

Chapter 16 expands the authentication side of that model into LDAP, SAML, OIDC, external identity mapping, credential rotation, and authentication troubleshooting—while preserving the least-privilege authorization model built here.

Official references and version notes

Version-sensitive statements were rechecked on 2026-08-26. Sonatype's current official self-hosted download page exposes Nexus Repository 3.95.0 (archive build 3.95.0-07), while version-index pages may lag. The mandatory chapter path is Community-compatible: local users, roles, privileges, realms, anonymous-access settings, Raw repositories, and temporary passwords. Self-hosted User Token Support is Pro-only and appears only as an optional extension or fixture.

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.