Chapter 14Lesson 05240–320 min

Checkpoint Lab — Content Selectors, Repository Targets, Fine-Grained Privileges, and Path-Level Authorization

Implement a two-team namespace policy, deliberately detect an overbroad group privilege, tighten it, prove the final read/write/delete matrix, and leave an auditable authorization evidence packet.

Checkpoint labTwo teamsAuthorization matrixEvidenceRemediation

Checkpoint objectives

  • Build one disposable Raw hosted/group topology containing two synthetic team namespaces.
  • Create selector-backed Team A and Team B policies and predict their effects before testing.
  • Prove read/write/delete outcomes through direct and group endpoints.
  • Detect and repair one deliberately overbroad group privilege.
  • Produce a secret-safe evidence packet and supported cleanup sequence.

Checkpoint safety boundary. Run only on a disposable local/private Nexus instance. Use synthetic users ch14-team-a/ch14-team-b, synthetic team-a/team-b paths, and temporary credential files. Never run these role experiments against production identities or repositories.

1. Scenario and target policy

One shared Raw hosted repository stores two team namespaces. Consumers read through one Raw group. Each team may read, add, and edit only its own namespace. Neither team may delete. Publishing happens only to hosted; the group is a read endpoint. Team A is initially given one deliberately broad group-read privilege so the test suite can discover and repair the exposure.

Operation Team A own Team A Team B Team B own Team B Team A
Direct hosted GET ALLOW DENY ALLOW DENY
Group GET after repair ALLOW DENY ALLOW DENY
Direct hosted PUT/add-edit ALLOW DENY ALLOW DENY
Direct DELETE via Assets API DENY DENY DENY DENY
PUT/DELETE to group URL REJECT / not a CE publish target REJECT REJECT REJECT

2. Preflight and evidence directory

export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch14-checkpoint"
mkdir -p "$LAB/evidence" "$LAB/payloads"
printf 'team-a release v1\n' > "$LAB/payloads/team-a.txt"
printf 'team-b release v1\n' > "$LAB/payloads/team-b.txt"

read -r -p 'Disposable Nexus admin user: ' NX_USER
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/repositories"   > "$LAB/evidence/repositories-before.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/content-selectors"   > "$LAB/evidence/selectors-before.json"
curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/roles"   > "$LAB/evidence/roles-before.json"

Record the actual Nexus version/edition/runtime in the evidence packet. A 3.95.2-compatible current instance is the reference baseline; the final report must state what actually ran.

3. Predictions before mutation

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

  1. Adding a Team A asset will create Raw repository metadata and blob-backed bytes under the hosted repository, while the group adds no second copy merely because it can read the member.
  2. A Team A selector privilege on the hosted repository will not automatically constrain a broad repository-wide read grant on the group.
  3. Omitting delete should cause Team A's Assets API delete attempt to be denied even for its own matching path.

4. Create the disposable repositories

Create academy-ch14cp-hosted as Raw hosted with the default blob store, strict content-type validation, and ALLOW write policy. Create academy-ch14cp-group as Raw group with only the hosted repository as a member. Use current UI or the Raw repository REST endpoints.

After creation, capture GET /service/rest/v1/repositories and confirm the type/format/member relationship before adding users.

5. Create selectors, privileges, roles, and users

Object Definition
Selector A format == "raw" and path =^ "/team-a/"
Selector B format == "raw" and path =^ "/team-b/"
A hosted privilege A selector + academy-ch14cp-hosted + browse/read/add/edit
A group privilege A selector + academy-ch14cp-group + browse/read
B hosted privilege B selector + hosted + browse/read/add/edit
B group privilege B selector + group + browse/read
Team A role — intentionally broken A hosted selector privilege + A group selector privilege + broad group read
Team B role B hosted selector privilege + B group selector privilege

Create local users ch14-team-a and ch14-team-b with only their corresponding roles. The broad group privilege is deliberate test data; do not reproduce it outside this disposable checkpoint.

6. Private team credential files

read -r -s -p 'Temporary password for ch14-team-a: ' TA_PASS; echo
export TA_AUTH="$(mktemp)"; chmod 600 "$TA_AUTH"
printf 'machine 127.0.0.1 login ch14-team-a password %s\n' "$TA_PASS" > "$TA_AUTH"
unset TA_PASS

read -r -s -p 'Temporary password for ch14-team-b: ' TB_PASS; echo
export TB_AUTH="$(mktemp)"; chmod 600 "$TB_AUTH"
printf 'machine 127.0.0.1 login ch14-team-b password %s\n' "$TB_PASS" > "$TB_AUTH"
unset TB_PASS

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

7. Publish the two owned assets and test cross-team writes

curl -sS --netrc-file "$TA_AUTH" --upload-file "$LAB/payloads/team-a.txt"   -o "$LAB/evidence/a-own-write.body" -w 'a own write %{http_code}\n'   "$HOSTED_URL/team-a/app/1.0.0/release.txt" | tee "$LAB/evidence/a-own-write.status"

curl -sS --netrc-file "$TB_AUTH" --upload-file "$LAB/payloads/team-b.txt"   -o "$LAB/evidence/b-own-write.body" -w 'b own write %{http_code}\n'   "$HOSTED_URL/team-b/app/1.0.0/release.txt" | tee "$LAB/evidence/b-own-write.status"

curl -sS --netrc-file "$TA_AUTH" --upload-file "$LAB/payloads/team-a.txt"   -o "$LAB/evidence/a-foreign-write.body" -w 'a foreign write %{http_code}\n'   "$HOSTED_URL/team-b/blocked.txt" | tee "$LAB/evidence/a-foreign-write.status"

curl -sS --netrc-file "$TB_AUTH" --upload-file "$LAB/payloads/team-b.txt"   -o "$LAB/evidence/b-foreign-write.body" -w 'b foreign write %{http_code}\n'   "$HOSTED_URL/team-a/blocked.txt" | tee "$LAB/evidence/b-foreign-write.status"

Verify with the admin Assets API that only the two intended assets exist. Checksums can prove the observed bytes, not provenance or authorization correctness.

8. Prove the intentionally broken group policy

for endpoint in hosted group; do
  base="$HOSTED_URL"; [ "$endpoint" = group ] && base="$GROUP_URL"
  for team in team-a team-b; do
    curl -sS --netrc-file "$TA_AUTH"       -o "$LAB/evidence/pre-$endpoint-$team.body"       -w "pre $endpoint $team %{http_code}\n"       "$base/$team/app/1.0.0/release.txt"       | tee "$LAB/evidence/pre-$endpoint-$team.status"
  done
done

The intended broken signature is: Team A cannot read Team B directly from hosted but can read Team B through the group because the role contains the broad group read privilege. Capture the Team A role JSON at this point.

9. Tighten the overbroad role and retest

Edit academy-ch14cp-team-a-role. Remove only the broad repository-view read privilege on academy-ch14cp-group. Keep the A-specific group content-selector privilege. Do not alter Selector A, Selector B, repository membership, or stored assets.

for team in team-a team-b; do
  curl -sS --netrc-file "$TA_AUTH"     -o "$LAB/evidence/post-group-$team.body"     -w "post group $team %{http_code}\n"     "$GROUP_URL/$team/app/1.0.0/release.txt"     | tee "$LAB/evidence/post-group-$team.status"
done

Expected final policy: Team A still reads Team A through the group, but Team B becomes denied. This is the chapter's central proof that selector policy must be evaluated over the principal's complete grant set and every client endpoint.

10. Prove delete is absent

Use the admin account only to discover the Team A asset ID, then attempt deletion as Team A:

curl -fsS --netrc-file "$NX_AUTH_FILE"   "$NX_URL/service/rest/v1/assets?repository=academy-ch14cp-hosted"   > "$LAB/evidence/assets-before-delete-test.json"

export TEAM_A_ASSET_ID="$(python - <<'PYI'
import json, os
from pathlib import Path
p=Path(os.environ['LAB'])/'evidence/assets-before-delete-test.json'
data=json.loads(p.read_text())
for item in data.get('items',[]):
    if item.get('path') == 'team-a/app/1.0.0/release.txt':
        print(item['id']); break
PYI
)"

test -n "$TEAM_A_ASSET_ID"
curl -sS --netrc-file "$TA_AUTH" -X DELETE   -o "$LAB/evidence/a-delete-own.body"   -w 'a delete own %{http_code}\n'   "$NX_URL/service/rest/v1/assets/$TEAM_A_ASSET_ID"   | tee "$LAB/evidence/a-delete-own.status"

# Verify the asset still downloads.
curl -fsS --netrc-file "$TA_AUTH"   "$HOSTED_URL/team-a/app/1.0.0/release.txt"   -o "$LAB/evidence/a-after-delete-attempt.txt"

Record the actual denial status. The asset must remain. If deletion succeeds, stop: the user has an unintended broader delete grant that must be found before cleanup.

11. Test write/delete against the group endpoint

curl -sS --netrc-file "$TA_AUTH" --upload-file "$LAB/payloads/team-a.txt"   -o "$LAB/evidence/group-put.body" -w 'group PUT %{http_code}\n'   "$GROUP_URL/team-a/group-write-must-not-succeed.txt"   | tee "$LAB/evidence/group-put.status"

curl -sS --netrc-file "$TA_AUTH" -X DELETE   -o "$LAB/evidence/group-delete.body" -w 'group DELETE %{http_code}\n'   "$GROUP_URL/team-a/app/1.0.0/release.txt"   | tee "$LAB/evidence/group-delete.status"

Do not require one exact status code across all reverse-proxy/client versions. Require the invariant: neither group request creates, edits, or removes the asset. In this Community/free course, hosted is the publish target and group is the consumer endpoint.

12. Required evidence packet

curl -fsS --netrc-file "$NX_AUTH_FILE" "$NX_URL/service/rest/v1/security/content-selectors"   > "$LAB/evidence/selectors-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/assets?repository=academy-ch14cp-hosted"   > "$LAB/evidence/assets-final.json"

python - <<'PYI'
from pathlib import Path
import json, os
lab=Path(os.environ['LAB'])
summary={
  'hosted':'academy-ch14cp-hosted',
  'group':'academy-ch14cp-group',
  'selectors':{
    'team-a':'format == "raw" and path =^ "/team-a/"',
    'team-b':'format == "raw" and path =^ "/team-b/"'
  },
  'team_actions':['browse','read','add','edit'],
  'delete_granted':False,
  'intentional_fault':'broad group read on Team A role',
  'repair':'remove broad group read; retain Team A group selector privilege',
  'group_deployment_required':False,
  'direct_blob_or_database_edits':False,
  'credentials_in_evidence':False
}
(lab/'evidence/checkpoint-summary.json').write_text(json.dumps(summary,indent=2)+'
')
PYI
find "$LAB/evidence" -maxdepth 1 -type f -printf '%f\n' 2>/dev/null | sort   > "$LAB/evidence/files.txt" || true

Review files before sharing. The admin and team netrc files live outside evidence/ and must never be copied into the evidence packet.

13. Verification checklist

  • Actual Nexus version/edition/runtime was recorded.
  • Selectors use leading-slash paths and =^ prefixes.
  • Each hosted/group selector privilege has only the intended actions.
  • Team A's deliberate broad group read caused the predicted pre-repair exposure.
  • Removing only that broad grant closed Team B through the group without breaking Team A.
  • Cross-team direct writes remain denied.
  • Own direct writes succeed; group writes do not create content.
  • Delete attempts remain denied and both owned assets still exist.
  • No wildcard/admin role remains on either team user.
  • No secret appears in URL, shell argument, evidence file, or screenshot.
  • No database or blob file was edited directly.

14. Supported cleanup and rollback

  1. Delete local users ch14-team-a and ch14-team-b.
  2. Delete the two disposable roles.
  3. Delete the four selector privileges and any deliberately broad test privilege if custom-created.
  4. Delete the two content selectors.
  5. Delete academy-ch14cp-group, then academy-ch14cp-hosted through Nexus-supported operations.
  6. Confirm names are absent from the repository/security APIs.
  7. Remove local credential files and the disposable workspace.
rm -f "$TA_AUTH" "$TB_AUTH" "$NX_AUTH_FILE"
unset TA_AUTH TB_AUTH NX_AUTH_FILE NX_USER HOSTED_URL GROUP_URL TEAM_A_ASSET_ID
rm -rf "$LAB"

15. Knowledge check

Why did Team A see Team B through the group before repair?

What single change repaired the exposure?

Why does the checkpoint test DELETE even though delete was never intended?

Why are group PUT/DELETE results recorded instead of forcing a particular status code?

What does Chapter 14 add to the operating model?

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

This chapter adds an authorization runbook to the artifact platform: namespace ownership, selector expressions, endpoint-specific privileges, role inheritance review, direct/group test matrices, and evidence-driven least-privilege remediation. The team can now prove what each identity can actually do instead of trusting a role name or UI screenshot.

Chapter 15 expands from path-level authorization into the broader identity system: users, roles, privileges, realms, anonymous access, user-token boundaries, and a complete least-privilege design.

Official references and version notes

Version-sensitive statements were rechecked on 2026-08-26. Sonatype's verified container registry exposes Nexus Repository 3.95.2 as the current latest image line. The mandatory chapter path uses Community-compatible content selectors, local users/roles, Raw hosted/group repositories, and the self-hosted security APIs. It does not require Pro user tokens, SAML/OIDC, HA, staging, Repository Firewall, or deployment to a group repository.

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.