Chapter 10Lesson 05220–290 min

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 labExact package identityInternal-only groupSymbol serverSafe cleanup

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:

  1. Publishing Learner.Ch10.Checkpoint/1.0.0 will create hosted package metadata plus .nupkg and .snupkg blob-backed assets.
  2. Restoring from a brand-new NUGET_PACKAGES folder through the group will read Nexus state; it cannot succeed from a prior client cache.
  3. A second push of the same ID/version will be rejected because hosted deployment policy is Disable redeploy.
  4. The package hash retrieved from the consumer cache will match the producer .nupkg hash.

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.Config with package mapping.
  • Producer .nupkg/.snupkg hashes.
  • 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.sha512 equality 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 .nupkg and .snupkg were 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?

What does the duplicate push failure prove?

What does matching producer and consumer SHA-256 prove?

Why is the snupkg version-gated?

Why keep API key and restore password separate?

What does Chapter 10 add to the production operating model?

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.

Next chapter

Operating-system and generic repository formats

Carry forward explicit package identity, repository-type boundaries, metadata evidence, client isolation, credential hygiene, and safe cleanup.

Official references and version notes

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.

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