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 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:
- 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.
- A Team A selector privilege on the hosted repository will not automatically constrain a broad repository-wide read grant on the group.
-
Omitting
deleteshould 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
-
Delete local users
ch14-team-aandch14-team-b. - Delete the two disposable roles.
- Delete the four selector privileges and any deliberately broad test privilege if custom-created.
- Delete the two content selectors.
-
Delete
academy-ch14cp-group, thenacademy-ch14cp-hostedthrough Nexus-supported operations. - Confirm names are absent from the repository/security APIs.
- 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?
A broad group read privilege independently authorized member content through the group. The narrow hosted selector could not revoke that grant.
What single change repaired the exposure?
Remove the broad group read from Team A and keep only its Team A selector-backed group read privilege.
Why does the checkpoint test DELETE even though delete was never intended?
Least privilege is proven by negative tests as well as positive tests; the absence of delete must be observable.
Why are group PUT/DELETE results recorded instead of forcing a particular status code?
The invariant is that the Community group is not the publish/delete target; transport layers can vary the exact rejection code.
What does Chapter 14 add to the operating model?
An auditable, testable path-authorization model that accounts for selectors, endpoint scope, role aggregation, and negative authorization tests.
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
- Sonatype: Content Selectors — CSEL syntax, preview, supported formats, performance guidance, and upload limitations.
- Sonatype: Privileges — repository view versus content-selector privileges, actions, additive grants, and group semantics.
- Sonatype: Access Control — RBAC object model.
- Sonatype: Security Management API and API Reference.
- Sonatype: Changes During the Upgrade Process — Nexus Repository 2 repository targets become Nexus Repository 3 content selectors.
- Self-Hosted Feature Matrix — Repo Targets / Content Selectors are Community and Pro capabilities.
- Sonatype: Database Options — coordinate-based selectors are deprecated with PostgreSQL and no longer supported.
- Sonatype verified Nexus Docker image tags.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.