Checkpoint Lab — NuGet Repositories, API Keys, Feeds, Symbols, Proxying, and .NET Package Workflows
Create a tiny .NET package, publish it and its symbols to disposable Nexus NuGet hosted storage, restore from an isolated group and clean package cache, prove package identity/source selection, exercise the current symbol path, and remove only disposable state.
Checkpoint objectives
- Build one synthetic NuGet package and companion symbols exactly once.
- Publish only to a disposable hosted repository with immutable deployment policy.
- Restore through an isolated group and fresh package cache using a one-source NuGet.Config.
- Prove package identity with producer and consumer hashes plus Nexus source evidence.
- Verify current symbol capability without weakening authentication or editing Nexus internals.
- Clean up only disposable repositories, identities, keys, client caches, and files.
Checkpoint boundary. Use only a disposable local
Nexus Repository Community Edition 3.95.2 instance. The client
baseline is .NET SDK 10.0.400. Use synthetic namespace
Learner.Ch10.Checkpoint. No production feeds, corporate
package IDs, CI secrets, public publication, direct database edits,
or blob-file deletion.
1. Scenario and predictions
You are onboarding a small .NET team. They need one immutable internal package, a clean consumer feed, and debugger symbols. Before acting, write predictions:
-
Publishing
Learner.Ch10.Checkpoint/1.0.0will create hosted package metadata plus.nupkgand.snupkgblob-backed assets. -
Restoring from a brand-new
NUGET_PACKAGESfolder through the group will read Nexus state; it cannot succeed from a prior client cache. - A second push of the same ID/version will be rejected because hosted deployment policy is Disable redeploy.
-
The package hash retrieved from the consumer cache will match the
producer
.nupkghash.
2. Preflight and exact assumptions
export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch10-checkpoint"
rm -rf "$LAB"
mkdir -p "$LAB/evidence" "$LAB/src" "$LAB/pkg" "$LAB/packages" "$LAB/http-cache"
chmod 700 "$LAB"
export NUGET_CONFIG="$LAB/NuGet.Config"
export NUGET_PACKAGES="$LAB/packages"
export NUGET_HTTP_CACHE_PATH="$LAB/http-cache"
dotnet --version | tee "$LAB/evidence/dotnet-version.txt"
dotnet --info > "$LAB/evidence/dotnet-info.txt"
curl -fsS "$NX_URL/service/rest/v1/status" | tee "$LAB/evidence/nexus-status.txt"
Expected baseline: Nexus 3.95.2 Community, Java 21, single node, H2 only as the disposable database, default file blob store, and .NET 10.0.400 client. If your versions differ, stop and reconcile feature support before continuing.
3. Create a checkpoint-only repository topology
| Name | Recipe | Purpose |
|---|---|---|
academy-ch10cp-hosted |
NuGet hosted, Disable redeploy | Authoritative internal package and symbols. |
academy-ch10cp-group |
NuGet group with only the hosted member | Internal-only consumer endpoint; deliberately no public proxy member. |
The internal-only group is intentional. It proves the checkpoint package cannot fall through to public nuget.org through this endpoint. Public dependencies are not needed by the package.
Create academy-ch10cp-publisher with read/browse on the
group and add/read/browse on the hosted repository plus the
nx-apikey-all privilege required to access the NuGet
API key. Activate NuGet API-Key Realm if needed.
4. One-source, V3, loopback-only NuGet.Config
cat > "$NUGET_CONFIG" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="CheckpointGroup"
value="http://127.0.0.1:8081/repository/academy-ch10cp-group/index.json"
protocolVersion="3"
allowInsecureConnections="true" />
</packageSources>
<packageSourceMapping>
<packageSource key="CheckpointGroup">
<package pattern="Learner.Ch10.*" />
</packageSource>
</packageSourceMapping>
</configuration>
EOF
chmod 600 "$NUGET_CONFIG"
dotnet nuget list source --configfile "$NUGET_CONFIG" | tee "$LAB/evidence/source-list.txt"
The mapping is intentionally narrow. This checkpoint package has no external dependencies, so every package request should match the internal pattern and remain on the checkpoint group.
5. Read and deployment credentials
read -r -p 'Checkpoint Nexus username: ' NX_USER
read -r -s -p 'Checkpoint Nexus password: ' NX_PASS; echo
export NuGetPackageSourceCredentials_CheckpointGroup="Username=${NX_USER};Password=${NX_PASS};ValidAuthenticationTypes=Basic"
unset NX_PASS
read -r -s -p 'Checkpoint NuGet API key: ' NUGET_API_KEY; echo
export NUGET_API_KEY
Never print these variables. The password authenticates ordinary repository reads; the API key is the deployment credential. The repository privilege matrix still decides what each identity may do.
6. Build exactly once with deterministic package settings
cd "$LAB/src"
dotnet new classlib --framework net10.0 --name Learner.Ch10.Checkpoint --no-restore
cd Learner.Ch10.Checkpoint
cat > Learner.Ch10.Checkpoint.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Learner.Ch10.Checkpoint</PackageId>
<Version>1.0.0</Version>
<Authors>DevOps Academy Checkpoint</Authors>
<Description>Disposable Nexus NuGet checkpoint</Description>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<Deterministic>true</Deterministic>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
</Project>
EOF
cat > Class1.cs <<'EOF'
namespace Learner.Ch10.Checkpoint;
public static class PackageIdentity
{
public const string Value = "Learner.Ch10.Checkpoint/1.0.0";
}
EOF
dotnet restore --configfile "$NUGET_CONFIG" -p:NuGetAudit=false
dotnet pack -c Release --no-restore -o "$LAB/pkg" 2>&1 | tee "$LAB/evidence/pack.txt"
ls -lh "$LAB/pkg" | tee "$LAB/evidence/package-files.txt"
Do not rebuild version 1.0.0 after this point. Promotion or later environment use must preserve these exact bytes.
7. Capture producer identity evidence
python - <<'PYI'
from pathlib import Path
import hashlib, base64, os
lab=Path(os.environ["LAB"])
rows=[]
for p in sorted((lab / "pkg").glob("*.*nupkg")):
data=p.read_bytes()
sha256=hashlib.sha256(data).hexdigest()
sha512_b64=base64.b64encode(hashlib.sha512(data).digest()).decode("ascii")
rows.append((p.name, sha256, sha512_b64))
(lab / "evidence" / "producer-sha256.txt").write_text("
".join(f"{h} {n}" for n,h,_ in rows)+"
")
(lab / "evidence" / "producer-nupkg-sha512-base64.txt").write_text(next(b+"
" for n,_,b in rows if n.endswith(".nupkg") and not n.endswith(".snupkg")))
for n,h,b in rows:
print(f"{h} {n}")
PYI
cat "$LAB/evidence/producer-sha256.txt"
The SHA-256 list is human-readable evidence for both package files.
The separate Base64 SHA-512 value matches the hash representation
NuGet records in the consumer global-packages folder for the
.nupkg, which lets the checkpoint prove byte identity
without assuming NuGet preserves the original archive file in that
cache.
8. Publish to hosted only
export HOSTED_V3="http://127.0.0.1:8081/repository/academy-ch10cp-hosted/index.json"
dotnet nuget push "$LAB/pkg/Learner.Ch10.Checkpoint.1.0.0.nupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" 2>&1 | tee "$LAB/evidence/push-nupkg.txt"
dotnet nuget push "$LAB/pkg/Learner.Ch10.Checkpoint.1.0.0.snupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" 2>&1 | tee "$LAB/evidence/push-snupkg.txt"
Immediately inspect Browse/Search and record the hosted repository, package ID, version, assets, upload time, and any available hashes. Do not alter blob/database files to “verify” storage.
9. Verify immutable deployment policy
set +e
dotnet nuget push "$LAB/pkg/Learner.Ch10.Checkpoint.1.0.0.nupkg" --source "$HOSTED_V3" --configfile "$NUGET_CONFIG" >"$LAB/evidence/duplicate.txt" 2>&1
rc=$?
set -e
printf 'duplicate exit=%s
' "$rc" | tee -a "$LAB/evidence/duplicate.txt"
test "$rc" -ne 0
Preserve the original error. The correct repair is not Allow redeploy; the correct release process uses a new version.
10. Restore through the internal-only group from an empty cache
rm -rf "$LAB/packages" "$LAB/http-cache"
mkdir -p "$LAB/packages" "$LAB/http-cache" "$LAB/consumer"
cd "$LAB/consumer"
cat > consumer.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Learner.Ch10.Checkpoint" Version="1.0.0" />
</ItemGroup>
</Project>
EOF
NUGET_PACKAGES="$LAB/packages" dotnet restore --configfile "$NUGET_CONFIG" --no-http-cache --force-evaluate -v diagnostic > "$LAB/evidence/restore-diagnostic.txt" 2>&1
grep -Ei 'academy-ch10cp-group|Learner.Ch10.Checkpoint' "$LAB/evidence/restore-diagnostic.txt" | head -80 | tee "$LAB/evidence/restore-route-extract.txt"
The group has no public proxy member, and the client config has no other source. A successful restore therefore proves the package came from the controlled Nexus internal path, not nuget.org.
11. Compare consumer package identity with producer bytes
python - <<'PYI' | tee "$LAB/evidence/consumer-package-hash-check.txt"
from pathlib import Path
import os
lab=Path(os.environ["LAB"])
root=lab / "packages" / "learner.ch10.checkpoint" / "1.0.0"
consumer_file=root / "learner.ch10.checkpoint.1.0.0.nupkg.sha512"
if not consumer_file.exists():
raise SystemExit(f"Expected NuGet hash metadata is missing: {consumer_file}")
producer=(lab / "evidence" / "producer-nupkg-sha512-base64.txt").read_text().strip()
consumer=consumer_file.read_text().strip()
print("producer_sha512_base64=", producer)
print("consumer_sha512_base64=", consumer)
print("identity_match=", producer == consumer)
if producer != consumer:
raise SystemExit("Restored package hash does not match producer package")
PYI
NuGet's global-packages cache records a
.nupkg.sha512 value even though it does not guarantee
retention of the original .nupkg archive. Matching that
recorded SHA-512 to the independently computed producer package hash
proves the restored package bytes correspond to the exact package
published in this checkpoint. This is byte-identity evidence, not a
vulnerability or provenance attestation.
12. Verify the symbol path appropriate to Nexus 3.95+
First confirm the .snupkg is present in hosted
repository state. Record the group symbol server base:
http://127.0.0.1:8081/repository/academy-ch10cp-group/symbols
For a live debugger-style check, optionally install
dotnet-symbol into $LAB/tools through a
controlled Nexus public-tooling proxy/group and use
--server-path against the endpoint above with the built
DLL. If that optional tool bootstrap would introduce a public
fallback into the internal-only checkpoint group, keep it separate.
The required proof is that Nexus 3.95+ accepted and indexed the
companion .snupkg and exposes the symbol-server
capability; do not weaken access controls to force a debugger
request.
13. Negative source-selection proof
Change the consumer to request
Learner.Ch10.DoesNotExist/1.0.0 and restore with the
same config. It must fail while evidence remains on the loopback
checkpoint group.
sed 's/Learner.Ch10.Checkpoint/Learner.Ch10.DoesNotExist/' consumer.csproj > missing.csproj
set +e
NUGET_PACKAGES="$LAB/packages-missing" dotnet restore missing.csproj --configfile "$NUGET_CONFIG" --no-http-cache --force-evaluate -v normal > "$LAB/evidence/missing-restore.txt" 2>&1
rc=$?
set -e
printf 'missing restore exit=%s
' "$rc" | tee -a "$LAB/evidence/missing-restore.txt"
test "$rc" -ne 0
if grep -qi 'api.nuget.org' "$LAB/evidence/missing-restore.txt"; then
echo 'FAIL: direct public NuGet access appeared' >&2
exit 1
fi
The expected result is “package not found” from the controlled route, not a public lookup.
14. Required evidence packet
- Nexus version/edition/status and Java runtime evidence.
- .NET SDK/NuGet client version evidence.
- Repository topology: hosted plus internal-only group.
- Redacted publisher privilege matrix and NuGet API-Key Realm state.
-
One-source V3
NuGet.Configwith package mapping. - Producer
.nupkg/.snupkghashes. - Successful hosted push output with no secret values.
- Nexus Browse/Search package/asset evidence.
- Expected duplicate-push failure.
- Fresh restore diagnostic route evidence.
-
Consumer
.nupkg.sha512equality with the independently computed producer package SHA-512. - Symbol package/server capability evidence.
- Missing-package failure with no public direct access.
- Cleanup checklist.
15. Cleanup and rollback
Delete academy-ch10cp-group then
academy-ch10cp-hosted through supported Nexus UI/API.
Delete the checkpoint publisher and regenerate/revoke its lab key if
the identity remains. Do not delete the default blob store because
it may contain earlier chapters. Do not run repair tasks or edit
database rows for cleanup.
unset NuGetPackageSourceCredentials_CheckpointGroup NUGET_API_KEY NX_USER
printf 'After preserving evidence, remove local checkpoint directory: %s
' "$LAB"
16. Final verification checklist
- Exactly one build produced version 1.0.0.
-
Both
.nupkgand.snupkgwere published only to hosted. - Hosted deployment policy rejected a duplicate version.
- The internal group contained no public proxy.
-
The client config had one V3 source ending in
/index.json. - Fresh restore succeeded from the internal group.
- Producer and consumer package SHA-256 values matched.
- A missing internal package failed without direct public access.
- No password/API key was written to project files or evidence.
- No Nexus blob/database internals were changed.
Knowledge check
Why does the checkpoint group contain only hosted?
It makes source selection provable: the internal package cannot be satisfied by a public proxy member through that endpoint.
What does the duplicate push failure prove?
That the hosted release coordinate is protected by Disable redeploy.
What does matching producer and consumer SHA-256 prove?
The restored nupkg bytes equal the package built before publication. It does not independently prove trust or vulnerability safety.
Why is the snupkg version-gated?
Nexus symbol-package and Symbol Server support was introduced in 3.95. Older versions should not be diagnosed as though the feature existed.
Why keep API key and restore password separate?
They represent different authentication uses and can have different lifecycle/privilege scopes.
What does Chapter 10 add to the production operating model?
A NuGet-specific model for explicit V3 feeds, immutable package publication, source mapping, separate read/publish credentials, cache evidence, and symbol lifecycle.
17. Production operating model and Chapter 11 bridge
You can now run a .NET package path as a controlled protocol: V3 service discovery, explicit source routing, immutable hosted publication, group consumption, least-privilege credentials, cache isolation, package hash evidence, and symbol support. The same artifact-manager principles remain stable even though the client protocol differs from Maven, npm, PyPI, and OCI.
Chapter 11 expands beyond language package managers into APT, Yum, Raw, RubyGems, Composer, and generic artifact formats, where repository metadata, signing, layout, and client behavior diverge further.
Official references and version notes
- Sonatype: NuGet Repositories — hosted/proxy/group, V2/V3, authentication, Chocolatey, and symbol-server support.
- Sonatype: Configure NuGet With Nexus and NuGet CLI Usage.
- Sonatype: Realms — NuGet API-Key Realm.
- Sonatype: Tasks — current NuGet symbol-index repair task and maintenance cautions.
- Microsoft: NuGet.Config reference and Package Source Mapping.
-
Microsoft: authenticated feeds
—
NuGetPackageSourceCredentials_*environment variables. - Microsoft: dotnet nuget push and symbol packages (.snupkg).
- Microsoft: NuGet HTTPS Everywhere.
- Microsoft: .NET 10 downloads.
Version-sensitive statements were rechecked on 2026-08-26. The
mandatory lab assumes a disposable self-hosted Nexus Repository
Community Edition 3.95.2 instance, Java 21 on the Nexus side, and
.NET 10 SDK 10.0.400 on the client side. The NuGet symbol/Chocolatey
features used here were introduced in Nexus 3.95.0. Record the
actual dotnet --info and Nexus version in evidence
before execution.
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.